From ae0842372ff6dd1437d026f82968a3749f555ff4 Mon Sep 17 00:00:00 2001 From: Sam Tunnicliffe Date: Thu, 23 Nov 2023 18:39:11 +0000 Subject: [PATCH] Implementation of Transactional Cluster Metadata as described in CEP-21 An overview of the core components can be found in the included TransactionalClusterMetadata.md patch by Alex Petrov, Marcus Eriksson and Sam Tunnicliffe; reviewed by Alex Petrov, Marcus Eriksson and Sam Tunnicliffe for CASSANDRA-18330 Co-authored-by: Marcus Eriksson Co-authored-by: Alex Petrov Co-authored-by: Sam Tunnicliffe --- .build/build-resolver.xml | 6 +- .build/cassandra-build-deps-template.xml | 4 + .build/parent-pom-template.xml | 10 +- .circleci/config.yml | 626 +-- .gitignore | 1 + CHANGES.txt | 1 + NEWS.txt | 59 + build.xml | 2 +- ci/harry_simulation.sh | 96 + conf/cassandra.yaml | 14 +- conf/harry-example.yaml | 95 + lib/harry-core-0.0.2-CASSANDRA-18768.jar | Bin 0 -> 458194 bytes pylib/cqlshlib/cql3handling.py | 4 +- pylib/cqlshlib/test/test_cqlsh_completion.py | 7 +- .../apache/cassandra/auth/AuthKeyspace.java | 91 +- .../auth/CIDRGroupsMappingLoader.java | 2 +- .../auth/CIDRGroupsMappingManager.java | 24 +- .../auth/CIDRPermissionsManager.java | 22 +- .../cassandra/auth/CassandraRoleManager.java | 26 +- .../apache/cassandra/auth/DCPermissions.java | 3 +- .../apache/cassandra/auth/IRoleManager.java | 13 +- .../cassandra/batchlog/BatchlogManager.java | 41 +- .../cassandra/cache/AutoSavingCache.java | 10 +- .../concurrent/NamedThreadFactory.java | 38 +- .../apache/cassandra/concurrent/Stage.java | 2 + .../config/CassandraRelevantProperties.java | 35 +- .../org/apache/cassandra/config/Config.java | 27 +- .../cassandra/config/DatabaseDescriptor.java | 257 +- .../apache/cassandra/cql3/QueryProcessor.java | 101 +- .../cassandra/cql3/functions/UDAggregate.java | 75 + .../cassandra/cql3/functions/UDFunction.java | 73 + .../cql3/functions/masking/ColumnMask.java | 79 + .../restrictions/StatementRestrictions.java | 5 +- .../cql3/statements/DescribeStatement.java | 14 +- .../cql3/statements/PropertyDefinitions.java | 2 +- .../schema/AlterKeyspaceStatement.java | 88 +- .../schema/AlterSchemaStatement.java | 71 +- .../schema/AlterTableStatement.java | 175 +- .../statements/schema/AlterTypeStatement.java | 5 +- .../statements/schema/AlterViewStatement.java | 5 +- .../schema/CreateAggregateStatement.java | 5 +- .../schema/CreateFunctionStatement.java | 4 +- .../schema/CreateIndexStatement.java | 8 +- .../schema/CreateKeyspaceStatement.java | 16 +- .../schema/CreateTableStatement.java | 20 +- .../schema/CreateTriggerStatement.java | 5 +- .../schema/CreateTypeStatement.java | 4 +- .../schema/CreateViewStatement.java | 9 +- .../schema/DropAggregateStatement.java | 4 +- .../schema/DropFunctionStatement.java | 5 +- .../statements/schema/DropIndexStatement.java | 5 +- .../schema/DropKeyspaceStatement.java | 5 +- .../statements/schema/DropTableStatement.java | 4 +- .../schema/DropTriggerStatement.java | 5 +- .../statements/schema/DropTypeStatement.java | 5 +- .../statements/schema/DropViewStatement.java | 5 +- .../statements/schema/TableAttributes.java | 4 +- .../db/AbstractMutationVerbHandler.java | 185 + .../cassandra/db/ColumnFamilyStore.java | 115 +- .../db/CounterMutationVerbHandler.java | 8 +- .../apache/cassandra/db/DiskBoundaries.java | 23 +- .../cassandra/db/DiskBoundaryManager.java | 84 +- .../org/apache/cassandra/db/Keyspace.java | 132 +- .../cassandra/db/MutationVerbHandler.java | 9 +- .../db/PartitionRangeReadCommand.java | 63 +- .../org/apache/cassandra/db/ReadCommand.java | 72 +- .../cassandra/db/ReadCommandVerbHandler.java | 142 +- .../cassandra/db/ReadRepairVerbHandler.java | 8 +- .../org/apache/cassandra/db/ReadResponse.java | 7 + .../db/SinglePartitionReadCommand.java | 54 +- .../cassandra/db/SizeEstimatesRecorder.java | 43 +- .../apache/cassandra/db/SystemKeyspace.java | 274 +- .../AbstractCommitLogSegmentManager.java | 7 +- .../db/compaction/CompactionManager.java | 31 +- .../compaction/CompactionStrategyManager.java | 6 + .../db/compaction/ShardManagerNoDisks.java | 4 +- .../compaction/UnifiedCompactionStrategy.java | 7 +- .../apache/cassandra/db/filter/RowFilter.java | 1 + .../cassandra/db/guardrails/Guardrail.java | 2 +- .../cassandra/db/lifecycle/Tracker.java | 10 +- .../memtable/AbstractAllocatorMemtable.java | 7 +- .../cassandra/db/memtable/Memtable.java | 11 +- .../db/memtable/ShardBoundaries.java | 13 +- .../db/partitions/PartitionUpdate.java | 81 +- .../CassandraCompressedStreamReader.java | 2 +- .../db/streaming/CassandraStreamReader.java | 80 +- .../apache/cassandra/db/view/TableViews.java | 47 +- .../apache/cassandra/db/view/ViewBuilder.java | 4 +- .../cassandra/db/view/ViewBuilderTask.java | 2 +- .../apache/cassandra/db/view/ViewManager.java | 35 +- .../apache/cassandra/db/view/ViewUtils.java | 17 +- .../db/virtual/ClusterMetadataLogTable.java | 87 + .../cassandra/db/virtual/LocalTable.java | 145 + .../cassandra/db/virtual/PeersTable.java | 200 + .../db/virtual/SystemViewsKeyspace.java | 3 + .../apache/cassandra/dht/AbstractBounds.java | 2 +- .../apache/cassandra/dht/BootStrapper.java | 106 +- .../cassandra/dht/BootstrapDiagnostics.java | 15 +- .../apache/cassandra/dht/BootstrapEvent.java | 10 +- .../cassandra/dht/ComparableObjectToken.java | 2 +- .../org/apache/cassandra/dht/Datacenters.java | 16 +- .../apache/cassandra/dht/IPartitioner.java | 4 +- .../cassandra/dht/LocalPartitioner.java | 4 +- .../dht/OrderPreservingPartitioner.java | 24 + .../org/apache/cassandra/dht/OwnedRanges.java | 139 + src/java/org/apache/cassandra/dht/Range.java | 28 + .../apache/cassandra/dht/RangeStreamer.java | 296 +- src/java/org/apache/cassandra/dht/Token.java | 27 + .../tokenallocator/OfflineTokenAllocator.java | 9 +- .../dht/tokenallocator/TokenAllocation.java | 80 +- .../CoordinatorBehindException.java | 15 +- .../exceptions/InvalidRoutingException.java | 62 + .../exceptions/RequestFailureReason.java | 17 +- .../exceptions/UnavailableException.java | 3 +- .../exceptions/WriteTimeoutException.java | 3 +- .../cassandra/gms/ApplicationState.java | 24 +- .../apache/cassandra/gms/EndpointState.java | 16 +- .../apache/cassandra/gms/FailureDetector.java | 17 +- .../apache/cassandra/gms/GossipDigestAck.java | 2 +- .../gms/GossipDigestAckVerbHandler.java | 11 +- .../gms/GossipDigestSynVerbHandler.java | 5 +- .../org/apache/cassandra/gms/Gossiper.java | 1065 ++--- .../apache/cassandra/gms/GossiperEvent.java | 7 +- .../apache/cassandra/gms/GossiperMBean.java | 2 + .../org/apache/cassandra/gms/NewGossiper.java | 178 + .../apache/cassandra/gms/VersionedValue.java | 93 +- .../cassandra/hints/HintVerbHandler.java | 12 +- .../apache/cassandra/hints/HintsCatalog.java | 4 + .../hints/HintsDispatchExecutor.java | 7 +- .../cassandra/hints/HintsDispatchTrigger.java | 2 +- .../apache/cassandra/hints/HintsReader.java | 7 +- .../index/SecondaryIndexManager.java | 7 +- .../apache/cassandra/index/TargetParser.java | 8 + .../index/internal/CassandraIndex.java | 11 +- .../sai/virtual/ColumnIndexesSystemView.java | 9 +- .../sai/virtual/SSTableIndexesSystemView.java | 7 +- .../index/sai/virtual/SegmentsSystemView.java | 5 +- .../io/sstable/CQLSSTableWriter.java | 28 +- .../io/sstable/SSTableSimpleIterator.java | 26 + .../io/sstable/format/SSTableReader.java | 5 +- .../format/SSTableReaderLoadingBuilder.java | 2 +- .../io/sstable/format/big/BigTableWriter.java | 1 - .../AbstractCloudMetadataServiceSnitch.java | 32 +- .../locator/AbstractReplicaCollection.java | 40 +- .../locator/AbstractReplicationStrategy.java | 267 +- .../locator/CMSPlacementStrategy.java | 155 + .../apache/cassandra/locator/Endpoints.java | 5 + .../cassandra/locator/EndpointsByReplica.java | 64 + .../cassandra/locator/EndpointsForRange.java | 7 +- .../cassandra/locator/EndpointsForToken.java | 32 +- .../locator/GossipingPropertyFileSnitch.java | 59 +- .../cassandra/locator/InetAddressAndPort.java | 32 + .../cassandra/locator/LocalStrategy.java | 57 +- .../cassandra/locator/MetaStrategy.java | 87 + .../locator/NetworkTopologyStrategy.java | 137 +- .../cassandra/locator/PendingRangeMaps.java | 212 - .../cassandra/locator/PropertyFileSnitch.java | 260 +- .../locator/RackInferringSnitch.java | 38 +- .../cassandra/locator/RangesByEndpoint.java | 75 + .../org/apache/cassandra/locator/Replica.java | 40 +- .../cassandra/locator/ReplicaLayout.java | 83 +- .../apache/cassandra/locator/ReplicaPlan.java | 221 +- .../cassandra/locator/ReplicaPlans.java | 394 +- .../cassandra/locator/SimpleStrategy.java | 72 +- ...taDiagnostics.java => SystemStrategy.java} | 33 +- .../cassandra/locator/TokenMetadata.java | 1611 -------- .../cassandra/locator/TokenMetadataEvent.java | 62 - .../cassandra/metrics/KeyspaceMetrics.java | 10 + .../cassandra/metrics/PaxosMetrics.java | 3 + .../cassandra/metrics/StorageMetrics.java | 2 + .../apache/cassandra/metrics/TCMMetrics.java | 154 + .../cassandra/metrics/TableMetrics.java | 2 + .../cassandra/net/InboundMessageHandler.java | 6 +- .../org/apache/cassandra/net/InboundSink.java | 31 +- .../org/apache/cassandra/net/Message.java | 127 +- .../apache/cassandra/net/MessageDelivery.java | 44 + .../cassandra/net/MessagingService.java | 50 +- .../cassandra/net/OutboundConnection.java | 10 +- .../cassandra/net/ResponseVerbHandler.java | 65 +- src/java/org/apache/cassandra/net/Verb.java | 40 + .../cassandra/repair/RepairCoordinator.java | 4 +- .../apache/cassandra/repair/RepairJob.java | 8 +- .../repair/RepairMessageVerbHandler.java | 23 + .../RepairOutOfTokenRangeException.java | 13 +- .../cassandra/repair/RepairSession.java | 2 +- .../cassandra/schema/ColumnMetadata.java | 61 + .../schema/DefaultSchemaUpdateHandler.java | 356 -- .../DistributedMetadataLogKeyspace.java | 284 ++ .../cassandra/schema/DistributedSchema.java | 281 +- .../cassandra/schema/DroppedColumn.java | 34 + .../cassandra/schema/IndexMetadata.java | 41 + .../org/apache/cassandra/schema/Indexes.java | 36 + .../cassandra/schema/KeyspaceMetadata.java | 77 +- .../cassandra/schema/KeyspaceParams.java | 35 +- .../apache/cassandra/schema/Keyspaces.java | 142 +- .../schema/MigrationCoordinator.java | 761 ---- .../schema/OfflineSchemaUpdateHandler.java | 96 - .../cassandra/schema/PartitionDenylist.java | 17 +- .../cassandra/schema/ReplicationParams.java | 187 +- .../org/apache/cassandra/schema/Schema.java | 764 +--- .../cassandra/schema/SchemaConstants.java | 3 +- .../cassandra/schema/SchemaDiagnostics.java | 44 +- .../apache/cassandra/schema/SchemaEvent.java | 9 +- .../cassandra/schema/SchemaKeyspace.java | 55 +- .../cassandra/schema/SchemaProvider.java | 190 +- .../schema/SchemaPullVerbHandler.java | 20 +- .../schema/SchemaPushVerbHandler.java | 21 +- .../schema/SchemaTransformation.java | 97 +- .../schema/SchemaTransformations.java | 29 +- .../cassandra/schema/SchemaUpdateHandler.java | 78 - .../schema/SchemaUpdateHandlerFactory.java | 35 - .../SchemaUpdateHandlerFactoryProvider.java | 65 - .../schema/SystemDistributedKeyspace.java | 153 +- .../org/apache/cassandra/schema/TableId.java | 35 +- .../cassandra/schema/TableMetadata.java | 171 +- .../cassandra/schema/TableMetadataRef.java | 131 +- .../schema/TableMetadataRefCache.java | 152 - .../apache/cassandra/schema/TableParams.java | 144 + .../org/apache/cassandra/schema/Tables.java | 38 + .../cassandra/schema/TriggerMetadata.java | 32 + .../org/apache/cassandra/schema/Triggers.java | 39 + .../org/apache/cassandra/schema/Types.java | 66 + .../cassandra/schema/UserFunctions.java | 64 + .../apache/cassandra/schema/ViewMetadata.java | 52 + .../org/apache/cassandra/schema/Views.java | 38 + .../service/AbstractWriteResponseHandler.java | 4 + .../service/ActiveRepairService.java | 118 +- .../cassandra/service/CassandraDaemon.java | 244 +- .../apache/cassandra/service/ClientState.java | 21 + .../apache/cassandra/service/ClientWarn.java | 22 +- .../cassandra/service/EchoVerbHandler.java | 3 +- .../PendingRangeCalculatorService.java | 98 - ...dingRangeCalculatorServiceDiagnostics.java | 66 - .../PendingRangeCalculatorServiceEvent.java | 75 - .../cassandra/service/RangeRelocator.java | 323 -- .../org/apache/cassandra/service/Rebuild.java | 253 ++ .../cassandra/service/StartupChecks.java | 64 +- .../cassandra/service/StorageProxy.java | 145 +- .../cassandra/service/StorageService.java | 3669 ++++------------- .../service/StorageServiceMBean.java | 7 +- .../service/WriteResponseHandler.java | 2 + .../disk/usage/DiskUsageBroadcaster.java | 1 + .../apache/cassandra/service/paxos/Paxos.java | 88 +- .../cassandra/service/paxos/PaxosPrepare.java | 14 +- .../cassandra/service/paxos/PaxosRepair.java | 39 +- .../service/paxos/cleanup/PaxosCleanup.java | 18 +- .../paxos/cleanup/PaxosCleanupRequest.java | 5 + .../cleanup/PaxosStartPrepareCleanup.java | 3 - .../service/paxos/uncommitted/PaxosRows.java | 2 +- .../uncommitted/UncommittedTableData.java | 4 +- .../paxos/v1/AbstractPaxosVerbHandler.java | 73 + .../service/paxos/v1/PrepareVerbHandler.java | 6 +- .../service/paxos/v1/ProposeVerbHandler.java | 6 +- .../service/reads/AbstractReadExecutor.java | 11 +- .../cassandra/service/reads/ReadCallback.java | 33 +- .../reads/ReplicaFilteringProtection.java | 3 +- .../service/reads/ResponseResolver.java | 1 - .../reads/range/ReplicaPlanIterator.java | 8 +- .../reads/repair/BlockingPartitionRepair.java | 54 +- .../reads/repair/BlockingReadRepair.java | 7 + .../repair/RowIteratorMergeListener.java | 20 +- .../cassandra/streaming/DataMovement.java | 111 + .../streaming/DataMovementVerbHandler.java | 112 + ...treamReceivedOutOfTokenRangeException.java | 49 + ...StreamRequestOutOfTokenRangeException.java | 36 + .../cassandra/streaming/StreamSession.java | 68 +- .../cassandra/tcm/AbstractLocalProcessor.java | 202 + .../tcm/AtomicLongBackedProcessor.java | 182 + .../apache/cassandra/tcm/CMSOperations.java | 199 + .../cassandra/tcm/CMSOperationsMBean.java | 45 + .../apache/cassandra/tcm/ClusterMetadata.java | 945 +++++ .../cassandra/tcm/ClusterMetadataService.java | 860 ++++ src/java/org/apache/cassandra/tcm/Commit.java | 407 ++ .../tcm/CurrentEpochRequestHandler.java | 42 + .../org/apache/cassandra/tcm/Discovery.java | 260 ++ src/java/org/apache/cassandra/tcm/Epoch.java | 202 + .../cassandra/tcm/EpochAwareDebounce.java | 82 + .../org/apache/cassandra/tcm/FetchCMSLog.java | 121 + .../apache/cassandra/tcm/FetchPeerLog.java | 90 + .../org/apache/cassandra/tcm/MetadataKey.java | 81 + .../apache/cassandra/tcm/MetadataKeys.java | 55 + .../cassandra/tcm/MetadataSnapshots.java | 121 + .../apache/cassandra/tcm/MetadataValue.java | 25 + .../cassandra/tcm/MultiStepOperation.java | 187 + .../apache/cassandra/tcm/NotCMSException.java | 12 +- .../cassandra/tcm/PaxosBackedProcessor.java | 219 + .../apache/cassandra/tcm/PeerLogFetcher.java | 108 + src/java/org/apache/cassandra/tcm/Period.java | 207 + .../org/apache/cassandra/tcm/Processor.java | 73 + .../cassandra/tcm/RecentlySealedPeriods.java | 140 + .../apache/cassandra/tcm/RemoteProcessor.java | 341 ++ src/java/org/apache/cassandra/tcm/Retry.java | 202 + src/java/org/apache/cassandra/tcm/Sealed.java | 128 + .../org/apache/cassandra/tcm/Startup.java | 500 +++ .../tcm/StubClusterMetadataService.java | 124 + .../cassandra/tcm/TCM_implementation.md | 75 + .../tcm/TransactionalClusterMetadata.md | 77 + .../apache/cassandra/tcm/Transformation.java | 320 ++ .../tcm/compatibility/GossipHelper.java | 392 ++ .../tcm/compatibility/TokenRingUtils.java | 222 + .../extensions/AbstractExtensionValue.java | 80 + .../cassandra/tcm/extensions/EpochValue.java | 54 + .../tcm/extensions/ExtensionKey.java | 71 + .../tcm/extensions/ExtensionValue.java | 21 +- .../cassandra/tcm/extensions/IntValue.java | 54 + .../cassandra/tcm/extensions/StringValue.java | 54 + .../listeners/ChangeListener.java} | 32 +- .../listeners/ClientNotificationListener.java | 128 + .../tcm/listeners/InitializationListener.java | 21 +- .../tcm/listeners/LegacyStateListener.java | 164 + .../cassandra/tcm/listeners/LogListener.java | 18 +- .../listeners/MetadataSnapshotListener.java | 51 + .../listeners/PlacementsChangeListener.java | 52 + .../tcm/listeners/SchemaListener.java | 57 + .../listeners/UpgradeMigrationListener.java | 23 +- .../org/apache/cassandra/tcm/log/Entry.java | 189 + .../apache/cassandra/tcm/log/LocalLog.java | 858 ++++ .../apache/cassandra/tcm/log/LogReader.java | 14 +- .../apache/cassandra/tcm/log/LogState.java | 267 ++ .../apache/cassandra/tcm/log/LogStorage.java | 55 + .../apache/cassandra/tcm/log/Replication.java | 260 ++ .../tcm/log/SystemKeyspaceStorage.java | 162 + .../cassandra/tcm/membership/Directory.java | 757 ++++ .../cassandra/tcm/membership/Location.java | 83 + .../tcm/membership/NodeAddresses.java | 145 + .../cassandra/tcm/membership/NodeId.java | 126 + .../cassandra/tcm/membership/NodeState.java | 46 + .../cassandra/tcm/membership/NodeVersion.java | 137 + .../tcm/migration/ClusterMetadataHolder.java | 95 + .../cassandra/tcm/migration/Election.java | 264 ++ .../tcm/migration/GossipCMSListener.java | 80 + .../tcm/migration/GossipProcessor.java | 42 + .../tcm/ownership/DataPlacement.java | 206 + .../tcm/ownership/DataPlacements.java | 277 ++ .../apache/cassandra/tcm/ownership/Delta.java | 138 + .../cassandra/tcm/ownership/EntireRange.java | 50 + .../cassandra/tcm/ownership/MovementMap.java | 165 + .../tcm/ownership/PlacementDeltas.java | 245 ++ .../tcm/ownership/PlacementForRange.java | 437 ++ .../tcm/ownership/PlacementProvider.java | 54 + .../ownership/PlacementTransitionPlan.java | 128 + .../tcm/ownership/PrimaryRangeComparator.java | 51 + .../tcm/ownership/ReplicationMap.java | 106 + .../cassandra/tcm/ownership/TokenMap.java | 290 ++ .../tcm/ownership/UniformRangePlacement.java | 349 ++ .../tcm/ownership/VersionedEndpoints.java | 210 + .../cassandra/tcm/sequences/AddToCMS.java | 208 + .../tcm/sequences/BootstrapAndJoin.java | 523 +++ .../tcm/sequences/BootstrapAndReplace.java | 500 +++ .../sequences/CancelCMSReconfiguration.java | 105 + .../tcm/sequences/DataMovements.java | 143 + .../tcm/sequences/InProgressSequences.java | 304 ++ .../cassandra/tcm/sequences/LeaveStreams.java | 47 + .../cassandra/tcm/sequences/LockedRanges.java | 451 ++ .../apache/cassandra/tcm/sequences/Move.java | 586 +++ .../tcm/sequences/ProgressBarrier.java | 575 +++ .../tcm/sequences/ReconfigureCMS.java | 367 ++ .../tcm/sequences/RemoveNodeStreams.java | 165 + .../tcm/sequences/ReplaceSameAddress.java | 99 + .../tcm/sequences/SequenceState.java | 124 + .../tcm/sequences/SingleNodeSequences.java | 168 + .../tcm/sequences/UnbootstrapAndLeave.java | 367 ++ .../tcm/sequences/UnbootstrapStreams.java | 233 ++ .../AsymmetricMetadataSerializer.java | 53 + .../tcm/serialization/MessageSerializers.java | 87 + .../tcm/serialization/MetadataSerializer.java | 22 + .../PartitionerAwareMetadataSerializer.java | 56 + ...DTAndFunctionsAwareMetadataSerializer.java | 63 + .../UDTAwareMetadataSerializer.java | 61 + .../VerboseMetadataSerializer.java | 53 + .../cassandra/tcm/serialization/Version.java | 97 + .../tcm/transformations/AlterSchema.java | 280 ++ .../transformations/ApplyPlacementDeltas.java | 148 + .../tcm/transformations/Assassinate.java | 133 + .../CancelInProgressSequence.java | 97 + .../transformations/CustomTransformation.java | 264 ++ .../tcm/transformations/ForceSnapshot.java | 81 + .../tcm/transformations/PrepareJoin.java | 355 ++ .../tcm/transformations/PrepareLeave.java | 320 ++ .../tcm/transformations/PrepareMove.java | 300 ++ .../tcm/transformations/PrepareReplace.java | 386 ++ .../tcm/transformations/Register.java | 216 + .../tcm/transformations/SealPeriod.java | 100 + .../tcm/transformations/Startup.java | 157 + .../tcm/transformations/Unregister.java | 104 + .../tcm/transformations/UnsafeJoin.java | 107 + .../cms/AdvanceCMSReconfiguration.java | 393 ++ .../cms/BaseMembershipTransformation.java | 95 + .../transformations/cms/FinishAddToCMS.java | 111 + .../tcm/transformations/cms/Initialize.java | 92 + .../transformations/cms/PreInitialize.java | 135 + .../cms/PrepareCMSReconfiguration.java | 289 ++ .../transformations/cms/RemoveFromCMS.java | 175 + .../transformations/cms/StartAddToCMS.java | 121 + .../org/apache/cassandra/tools/NodeProbe.java | 28 +- .../org/apache/cassandra/tools/NodeTool.java | 5 + .../tools/SSTableExpiredBlockers.java | 4 +- .../apache/cassandra/tools/SSTableExport.java | 1 + .../cassandra/tools/SSTableLevelResetter.java | 8 +- .../tools/SSTableOfflineRelevel.java | 6 +- .../tools/StandaloneSSTableUtil.java | 7 +- .../cassandra/tools/StandaloneScrubber.java | 5 +- .../cassandra/tools/StandaloneSplitter.java | 6 +- .../cassandra/tools/StandaloneUpgrader.java | 8 +- .../cassandra/tools/StandaloneVerifier.java | 8 +- .../tools/TransformClusterMetadataHelper.java | 95 + src/java/org/apache/cassandra/tools/Util.java | 2 +- .../tools/nodetool/AbortBootstrap.java | 48 + .../cassandra/tools/nodetool/DescribeCMS.java | 44 + .../tools/nodetool/InitializeCMS.java | 29 +- .../apache/cassandra/tools/nodetool/Join.java | 2 - .../tools/nodetool/ReconfigureCMS.java | 122 + .../cassandra/tools/nodetool/RemoveNode.java | 21 +- .../cassandra/tools/nodetool/SealPeriod.java | 14 +- .../cassandra/tracing/TraceKeyspace.java | 63 +- .../apache/cassandra/utils/BiMultiValMap.java | 16 + .../cassandra/utils/CassandraVersion.java | 1 + .../org/apache/cassandra/utils/CounterId.java | 4 +- .../apache/cassandra/utils/FBUtilities.java | 29 +- .../apache/cassandra/utils/NativeLibrary.java | 5 + .../cassandra/utils/RecomputingSupplier.java | 125 - .../utils/btree/AbstractBTreeMap.java | 157 + .../cassandra/utils/btree/BTreeBiMap.java | 102 + .../cassandra/utils/btree/BTreeMap.java | 232 ++ .../cassandra/utils/btree/BTreeMultimap.java | 214 + .../cassandra/utils/btree/BTreeSet.java | 45 +- .../utils/concurrent/AbstractFuture.java | 33 + .../utils/concurrent/AsyncFuture.java | 11 + .../cassandra/utils/concurrent/Future.java | 10 + .../utils/concurrent/LoadingMap.java | 7 + .../utils/concurrent/SyncFuture.java | 11 + .../cassandra/utils/concurrent/WaitQueue.java | 1 + test/conf/logback-dtest.xml | 5 +- test/conf/logback-simulator.xml | 1 - .../apache/cassandra/distributed/Cluster.java | 41 +- .../cassandra/distributed/Constants.java | 4 + .../distributed/action/GossipHelper.java | 282 +- .../distributed/api/ICoordinator.java | 22 + ...urrentQuiescentCheckerIntegrationTest.java | 123 + .../distributed/fuzz/HarryHelper.java | 67 +- .../fuzz/InJVMTokenAwareVisitorExecutor.java | 111 + .../cassandra/distributed/fuzz/InJvmSut.java | 43 +- .../distributed/fuzz/InJvmSutBase.java | 43 +- .../distributed/harry/ClusterState.java | 24 + .../distributed/harry/ExistingClusterSUT.java | 96 + .../distributed/harry/FlaggedRunner.java | 85 + .../distributed/impl/AbstractCluster.java | 49 +- .../impl/DistributedTestSnitch.java | 2 +- .../cassandra/distributed/impl/Instance.java | 452 +- .../cassandra/distributed/impl/Listen.java | 7 - .../cassandra/distributed/impl/Query.java | 7 - .../distributed/impl/TestChangeListener.java | 119 + .../distributed/impl/UnsafeGossipHelper.java | 8 +- .../mock/nodetool/InternalNodeProbe.java | 2 + .../distributed/shared/ClusterUtils.java | 469 ++- .../distributed/shared/PreventSystemExit.java | 44 + .../cassandra/distributed/test/AlterTest.java | 12 - .../cassandra/distributed/test/AuthTest.java | 75 +- .../test/ByteBuddyExamplesTest.java | 15 +- .../distributed/test/CASAddTest.java | 18 +- .../distributed/test/CASContentionTest.java | 6 + .../distributed/test/CASMultiDCTest.java | 6 + .../cassandra/distributed/test/CASTest.java | 6 + .../distributed/test/CASTestBase.java | 24 +- .../test/CompactionDiskSpaceTest.java | 2 +- .../test/CreateTableNonDeterministicTest.java | 89 + .../distributed/test/DecommissionTest.java | 71 +- .../distributed/test/FailureLoggingTest.java | 19 - .../distributed/test/FrozenUDTTest.java | 5 +- .../distributed/test/GossipTest.java | 287 +- .../test/HintedHandoffAddRemoveNodesTest.java | 80 +- .../distributed/test/IPMembershipTest.java | 11 +- .../InternodeEncryptionEnforcementTest.java | 128 +- .../distributed/test/JVMDTestTest.java | 8 +- .../test/MigrationCoordinatorTest.java | 123 - .../distributed/test/MixedModeFuzzTest.java | 5 +- .../cassandra/distributed/test/MoveTest.java | 5 +- .../test/PartitionDenylistTest.java | 1 - .../distributed/test/PaxosRepair2Test.java | 5 +- .../distributed/test/PaxosRepairTest.java | 38 +- .../test/PaxosUncommittedIndexTest.java | 2 +- .../distributed/test/ReadRepairTest.java | 61 +- .../distributed/test/RemoveNodeTest.java | 99 + .../test/RepairCoordinatorFast.java | 22 +- .../test/RepairCoordinatorNeighbourDown.java | 24 +- .../test/RepairDigestTrackingTest.java | 54 +- .../distributed/test/RepairErrorsTest.java | 12 +- .../test/SSTableIdGenerationTest.java | 6 + .../distributed/test/SchemaTest.java | 161 +- .../distributed/test/SecondaryIndexTest.java | 2 +- .../distributed/test/TestBaseImpl.java | 31 +- .../test/TransientRangeMovement2Test.java | 134 + .../test/TransientRangeMovementTest.java | 272 ++ .../UpdateSystemAuthAfterDCExpansionTest.java | 33 +- .../guardrails/GuardrailDiskUsageTest.java | 26 +- ...lItemsPerCollectionOnSSTableWriteTest.java | 9 +- .../AssassinatedEmptyNodeTest.java | 11 +- .../hostreplacement/BaseAssassinatedCase.java | 8 +- .../hostreplacement/FailedBootstrapTest.java | 48 +- .../HostReplacementOfDownedClusterTest.java | 57 +- .../hostreplacement/HostReplacementTest.java | 18 +- ...ernatingNodeWithoutReplaceAddressTest.java | 16 +- .../test/jmx/JMXGetterCheckTest.java | 9 +- .../test/log/BootWithMetadataTest.java | 140 + .../test/log/BounceGossipTest.java | 247 ++ .../test/log/BounceIndexRebuildTest.java | 52 + .../test/log/BounceResetHostIdTest.java | 52 + .../test/log/CMSMembershipMetricsTest.java | 119 + .../distributed/test/log/CMSTestBase.java | 131 + .../test/log/ClusterMetadataTestHelper.java | 1078 +++++ .../log/ConflictingAddressRestartTest.java | 85 + .../test/log/ConsistentLeaveTest.java | 154 + .../test/log/ConsistentMoveTest.java | 163 + .../test/log/CoordinatorPathTest.java | 260 ++ .../test/log/CoordinatorPathTestBase.java | 1152 ++++++ .../distributed/test/log/DiscoverCMSTest.java | 84 + .../test/log/DistributedLogTest.java | 200 + .../distributed/test/log/FailedLeaveTest.java | 174 + .../test/log/FetchLogFromPeersTest.java | 367 ++ .../test/log/ForceSnapshotTest.java | 157 + .../distributed/test/log/FuzzTestBase.java | 142 + .../test/log/GossipDeadlockTest.java | 147 + .../InProgressSequenceCoordinationTest.java | 406 ++ .../log/MetadataChangeSimulationTest.java | 763 ++++ .../distributed/test/log/ModelChecker.java | 307 ++ .../distributed/test/log/ModelState.java | 364 ++ .../test/log/OperationalEquivalenceTest.java | 204 + .../test/log/PauseCommitsTest.java | 56 + .../test/log/PlacementSimulator.java | 1789 ++++++++ .../test/log/PlacementSimulatorTest.java | 379 ++ .../log/QuorumIntersectionSimulatorTest.java | 142 + .../test/log/ReconfigureCMSTest.java | 141 + .../distributed/test/log/RegisterTest.java | 169 + .../test/log/RequestCurrentEpochTest.java | 102 + .../test/log/ResumableStartupTest.java | 150 + .../distributed/test/log/RngUtils.java | 106 + .../test/log/SimulatedOperation.java | 562 +++ .../distributed/test/log/SnapshotTest.java | 244 ++ .../test/log/SystemKeyspaceStorageTest.java | 235 ++ .../distributed/test/log/TestProcessor.java | 137 + .../test/log/TriggeredReconfigureCMSTest.java | 151 + .../AssignSameTokenToMultipleNodesTest.java | 52 + .../test/ring/BootstrapResetProgressTest.java | 156 + .../distributed/test/ring/BootstrapTest.java | 193 +- .../test/ring/CMSMembershipTest.java | 185 + ...va => CleanupDuringRangeMovementTest.java} | 77 +- .../CommunicationDuringDecommissionTest.java | 18 +- .../test/ring/ConsistentBootstrapTest.java | 251 ++ .../test/ring/DecommissionTest.java | 209 + .../test/ring/NodeNotInRingTest.java | 3 +- .../test/ring/PendingWritesTest.java | 109 - .../test/ring/RangeVersioningTest.java | 72 + .../test/ring/ReadsDuringBootstrapTest.java | 115 - .../ring/StopProcessingExceptionTest.java | 78 + .../streaming/StreamCloseInMiddleTest.java | 22 +- .../test/tcm/LogReplicationSmokeTest.java | 101 + .../topology/DecommissionAvoidTimeouts.java | 23 +- .../distributed/upgrade/BatchUpgradeTest.java | 2 + .../ClusterMetadataUpgradeHarryTest.java | 143 + .../upgrade/ClusterMetadataUpgradeTest.java | 223 + .../CompactStorageColumnDeleteTest.java | 2 + .../CompactStorageHiddenColumnTest.java | 2 + ...ctStorageImplicitNullInClusteringTest.java | 2 + .../upgrade/CompactStoragePagingTest.java | 2 + ...mpactionHistorySystemTableUpgradeTest.java | 2 + .../upgrade/DropCompactStorageTest.java | 1 + .../upgrade/MixedModeBatchTestBase.java | 2 + .../upgrade/MixedModeConsistencyV30Test.java | 4 +- .../MixedModeFrom3ReplicationTest.java | 2 + .../upgrade/MixedModeIndexTestBase.java | 4 +- .../upgrade/MixedModeRepairTest.java | 2 +- .../MixedModeTTLOverflowUpgradeTest.java | 2 + .../upgrade/MixedModeWritetimeOrTTLTest.java | 2 + .../upgrade/Pre40MessageFilterTest.java | 2 +- .../distributed/upgrade/UpgradeTestBase.java | 39 +- .../byterewrite/StatusChangeListener.java | 133 - .../apache/cassandra/cql3/CorruptionTest.java | 15 +- .../db/commitlog/CommitLogStressTest.java | 2 +- .../db/compaction/LongCompactionsTest.java | 2 +- .../LongLeveledCompactionStrategyCQLTest.java | 3 +- .../LongLeveledCompactionStrategyTest.java | 29 +- .../io/sstable/CQLSSTableWriterLongTest.java | 10 - .../DynamicEndpointSnitchLongTest.java | 2 + .../streaming/LongStreamingTest.java | 5 +- .../test/microbench/MutationBench.java | 22 +- .../test/microbench/PendingRangesBench.java | 115 - .../asm/GlobalMethodTransformer.java | 8 +- .../simulator/asm/InterceptAgent.java | 6 +- .../systems/InterceptorOfSystemMethods.java | 13 +- .../cassandra/simulator/ActionList.java | 12 + .../apache/cassandra/simulator/Actions.java | 6 +- .../simulator/ClusterSimulation.java | 76 +- .../org/apache/cassandra/simulator/Debug.java | 12 +- .../apache/cassandra/simulator/Ordered.java | 4 +- .../cassandra/simulator/SimulationRunner.java | 2 - .../cassandra/simulator/SimulatorUtils.java | 9 + .../simulator/cluster/ClusterActions.java | 57 +- .../simulator/cluster/KeyspaceActions.java | 216 +- .../simulator/cluster/OnClusterChangeRf.java | 5 +- .../cluster/OnClusterChangeTopology.java | 5 +- .../simulator/cluster/OnClusterJoin.java | 88 +- .../simulator/cluster/OnClusterLeave.java | 96 +- .../simulator/cluster/OnClusterReplace.java | 147 +- .../cluster/OnClusterUpdateGossip.java | 64 - .../cluster/OnInstanceBootstrap.java | 54 - .../cluster/OnInstanceGossipWith.java | 50 - .../simulator/cluster/OnInstanceRepair.java | 8 +- .../OnInstanceSyncSchemaForBootstrap.java | 37 - ...nceSyncPendingRanges.java => Quiesce.java} | 35 +- .../cassandra/simulator/cluster/Utils.java | 23 +- .../simulator/harry/HarryValidatingQuery.java | 147 + .../systems/InterceptingAwaitable.java | 6 +- .../systems/InterceptingExecutor.java | 3 +- .../systems/InterceptingGlobalMethods.java | 3 + .../systems/InterceptingMonitors.java | 5 +- .../simulator/systems/SimulatedAction.java | 26 +- .../systems/SimulatedActionCallable.java | 4 +- .../systems/SimulatedActionTask.java | 8 + .../systems/SimulatedFailureDetector.java | 6 + .../simulator/systems/SimulatedSnitch.java | 7 + .../simulator/systems/SimulatedSystems.java | 12 +- .../test/AlwaysDeliverNetworkScheduler.java | 77 + .../test/FixedLossNetworkScheduler.java | 158 + .../simulator/test/HarrySimulatorTest.java | 797 ++++ .../test/ShortPaxosSimulationTest.java | 68 + .../test/SimulatedVisitExectuor.java | 182 + .../simulator/test/SimulationTestBase.java | 122 +- .../simulator/test/TrivialSimulationTest.java | 1 + .../org/apache/cassandra/SchemaLoader.java | 37 +- .../org/apache/cassandra/ServerTestUtils.java | 166 +- test/unit/org/apache/cassandra/Util.java | 106 +- .../auth/AllowAllCIDRAuthorizerTest.java | 3 - .../auth/CassandraAuthorizerTest.java | 3 +- ...assandraCIDRAuthorizerEnforceModeTest.java | 2 - ...assandraCIDRAuthorizerMonitorModeTest.java | 2 - .../auth/CassandraNetworkAuthorizerTest.java | 1 - .../auth/CassandraRoleManagerTest.java | 9 +- .../auth/CreateAndAlterRoleTest.java | 6 +- .../cassandra/auth/GrantAndRevokeTest.java | 3 +- .../auth/MutualTlsAuthenticatorTest.java | 5 +- .../MutualTlsInternodeAuthenticatorTest.java | 5 +- ...WithPasswordFallbackAuthenticatorTest.java | 4 +- .../auth/PasswordAuthenticatorTest.java | 6 +- .../cassandra/auth/RoleOptionsTest.java | 2 +- .../batchlog/BatchlogManagerTest.java | 31 +- .../cql3/AlterSchemaStatementTest.java | 67 + .../org/apache/cassandra/cql3/BatchTest.java | 2 +- .../cassandra/cql3/CDCStatementTest.java | 3 +- .../org/apache/cassandra/cql3/CQLTester.java | 97 +- .../cql3/CustomNowInSecondsTest.java | 6 +- .../cassandra/cql3/KeyCacheCqlTest.java | 10 +- .../apache/cassandra/cql3/OutOfSpaceTest.java | 25 +- .../org/apache/cassandra/cql3/PagingTest.java | 16 +- .../cassandra/cql3/RandomSchemaTest.java | 29 + .../org/apache/cassandra/cql3/ViewTest.java | 4 +- .../functions/masking/ColumnMaskTester.java | 1 - .../masking/SelectMaskedPermissionTest.java | 1 - .../masking/UnmaskPermissionTest.java | 1 - .../selection/SelectionColumnMappingTest.java | 6 +- .../statements/DescribeStatementTest.java | 12 +- .../entities/FrozenCollectionsTest.java | 5 +- .../cql3/validation/entities/JsonTest.java | 10 +- .../entities/SecondaryIndexTest.java | 9 +- .../validation/entities/UserTypesTest.java | 7 +- .../validation/entities/VirtualTableTest.java | 6 +- .../validation/operations/AlterNTSTest.java | 37 +- .../cql3/validation/operations/AlterTest.java | 209 +- .../operations/CompactStorageSplit2Test.java | 1 - .../validation/operations/CreateTest.java | 96 +- .../DropRecreateAndRestoreTest.java | 3 + ...nsertUpdateIfConditionCollectionsTest.java | 11 +- .../InsertUpdateIfConditionStaticsTest.java | 11 +- .../InsertUpdateIfConditionTest.java | 35 +- .../operations/SelectLimitTest.java | 9 +- .../org/apache/cassandra/db/CellSpecTest.java | 1 + .../org/apache/cassandra/db/CleanupTest.java | 86 +- .../cassandra/db/CleanupTransientTest.java | 29 +- .../db/ColumnFamilyStoreClientModeTest.java | 21 +- .../cassandra/db/ColumnFamilyStoreTest.java | 7 + .../org/apache/cassandra/db/ColumnsTest.java | 4 +- .../apache/cassandra/db/CounterCacheTest.java | 26 +- ...nterMutationVerbHandlerOutOfRangeTest.java | 192 + .../cassandra/db/DiskBoundaryManagerTest.java | 32 +- .../org/apache/cassandra/db/ImportTest.java | 98 +- .../org/apache/cassandra/db/KeyspaceTest.java | 21 +- .../db/MutationVerbHandlerOutOfRangeTest.java | 221 + .../apache/cassandra/db/ReadCommandTest.java | 5 +- .../ReadCommandVerbHandlerOutOfRangeTest.java | 272 ++ .../db/ReadCommandVerbHandlerTest.java | 30 +- .../apache/cassandra/db/ReadResponseTest.java | 14 +- .../db/RecoveryManagerFlushedTest.java | 23 +- .../cassandra/db/RepairedDataInfoTest.java | 4 +- .../org/apache/cassandra/db/RowCacheTest.java | 35 +- .../cassandra/db/SchemaCQLHelperTest.java | 8 - .../cassandra/db/SecondaryIndexTest.java | 13 +- .../db/SinglePartitionSliceCommandTest.java | 2 +- .../cassandra/db/SystemKeyspaceTest.java | 14 +- .../cassandra/db/TopPartitionTrackerTest.java | 22 +- .../CommitLogSegmentManagerCDCTest.java | 2 +- .../cassandra/db/commitlog/CommitLogTest.java | 57 +- .../db/commitlog/CommitLogUpgradeTest.java | 22 +- .../compaction/CompactionControllerTest.java | 3 +- .../db/compaction/CompactionIteratorTest.java | 32 +- ...tionStrategyManagerBoundaryReloadTest.java | 32 +- .../CompactionStrategyManagerTest.java | 9 +- .../db/compaction/LeveledGenerationsTest.java | 1 - .../db/compaction/NeverPurgeTest.java | 6 +- .../db/compaction/PartialCompactionsTest.java | 6 +- .../db/compaction/ShardManagerTest.java | 11 +- .../TimeWindowCompactionStrategyTest.java | 6 +- .../UnifiedCompactionStrategyTest.java | 6 +- .../db/compaction/unified/ControllerTest.java | 7 +- .../unified/ShardedCompactionWriterTest.java | 16 +- .../unified/ShardedMultiWriterTest.java | 1 - .../writers/CompactionAwareWriterTest.java | 6 +- .../db/context/CounterContextTest.java | 7 + .../cassandra/db/filter/ColumnFilterTest.java | 16 +- .../cassandra/db/filter/RowFilterTest.java | 1 + .../db/guardrails/GuardrailKeyspacesTest.java | 10 - ...GuardrailMaximumReplicationFactorTest.java | 6 +- ...GuardrailMinimumReplicationFactorTest.java | 7 +- .../db/guardrails/GuardrailTablesTest.java | 3 +- .../db/guardrails/GuardrailTester.java | 9 +- .../cassandra/db/lifecycle/HelpersTest.java | 5 +- .../lifecycle/LifecycleTransactionTest.java | 4 +- .../db/lifecycle/RealTransactionsTest.java | 5 +- .../cassandra/db/lifecycle/TrackerTest.java | 3 +- .../cassandra/db/lifecycle/ViewTest.java | 5 +- .../db/memtable/MemtableQuickTest.java | 4 +- .../db/memtable/MemtableSizeTestBase.java | 1 - .../db/partitions/PurgeFunctionTest.java | 1 + .../AbstractPendingAntiCompactionTest.java | 14 +- .../PartitionSerializationExceptionTest.java | 5 +- .../cassandra/db/rows/RowsMergingTest.java | 3 +- .../rows/ThrottledUnfilteredIteratorTest.java | 14 +- .../db/rows/UnfilteredRowIteratorsTest.java | 1 + .../db/streaming/StreamRequestTest.java | 3 + .../db/transform/RTTransformationsTest.java | 2 + .../db/view/ViewBuilderTaskTest.java | 2 +- .../cassandra/db/view/ViewUtilsTest.java | 68 +- .../db/virtual/BatchMetricsTableTest.java | 7 - .../CIDRFilteringMetricsTableTest.java | 2 - .../db/virtual/CQLMetricsTableTest.java | 7 - .../db/virtual/ClientsTableTest.java | 7 - .../CredentialsCacheKeysTableTest.java | 4 +- .../db/virtual/GossipInfoTableTest.java | 7 - .../JmxPermissionsCacheKeysTableTest.java | 3 +- .../db/virtual/LocalRepairTablesTest.java | 2 - .../db/virtual/LogMessagesTableTest.java | 7 - .../NetworkPermissionsCacheKeysTableTest.java | 3 +- .../PermissionsCacheKeysTableTest.java | 3 +- .../db/virtual/RolesCacheKeysTableTest.java | 3 +- .../db/virtual/SSTableTasksTableTest.java | 3 +- .../db/virtual/SettingsTableTest.java | 7 - .../db/virtual/StreamingVirtualTableTest.java | 1 - .../db/virtual/SystemPropertiesTableTest.java | 7 - .../cassandra/dht/BootStrapperTest.java | 103 +- .../cassandra/dht/LengthPartitioner.java | 2 +- .../tokenallocator/TokenAllocationTest.java | 129 +- .../cassandra/gms/ExpireEndpointTest.java | 65 - .../cassandra/gms/FailureDetectorTest.java | 26 +- .../cassandra/gms/GossipShutdownTest.java | 6 +- .../apache/cassandra/gms/GossiperTest.java | 96 +- .../apache/cassandra/gms/NewGossiperTest.java | 154 + .../PendingRangeCalculatorServiceTest.java | 134 - .../cassandra/gms/SerializationsTest.java | 32 +- .../apache/cassandra/gms/ShadowRoundTest.java | 218 - .../apache/cassandra/hints/AlteredHints.java | 2 +- .../cassandra/hints/DTestSerializer.java | 6 +- .../cassandra/hints/HintMessageTest.java | 2 +- .../org/apache/cassandra/hints/HintTest.java | 41 +- .../cassandra/hints/HintsReaderTest.java | 6 +- .../cassandra/hints/HintsStoreTest.java | 2 +- .../index/internal/CustomCassandraIndex.java | 37 +- .../sai/memory/VectorMemoryIndexTest.java | 26 +- .../sai/virtual/IndexesSystemViewTest.java | 3 - .../sai/virtual/SSTablesSystemViewTest.java | 3 - .../sasi/disk/PerSSTableIndexWriterTest.java | 3 +- .../index/sasi/plan/OperationTest.java | 3 +- .../io/compress/CQLCompressionTest.java | 4 +- .../sstable/CQLSSTableWriterClientTest.java | 22 +- .../CQLSSTableWriterConcurrencyTest.java | 7 - .../io/sstable/CQLSSTableWriterTest.java | 2 + .../io/sstable/LegacySSTableTest.java | 5 +- .../sstable/RangeAwareSSTableWriterTest.java | 7 +- .../io/sstable/SSTableLoaderTest.java | 8 +- .../io/sstable/SSTableWriterTestBase.java | 4 +- .../cassandra/io/sstable/VerifyTest.java | 20 +- .../locator/AlibabaCloudSnitchTest.java | 34 +- .../AssureSufficientLiveNodesTest.java | 20 +- .../locator/CloudstackSnitchTest.java | 42 +- .../locator/DynamicEndpointSnitchTest.java | 20 + .../cassandra/locator/Ec2SnitchTest.java | 34 +- .../locator/GoogleCloudSnitchTest.java | 34 +- .../cassandra/locator/MetaStrategyTest.java | 161 + .../locator/NetworkTopologyStrategyTest.java | 259 +- .../locator/PendingRangeMapsTest.java | 108 - .../cassandra/locator/PendingRangesTest.java | 273 +- .../locator/PropertyFileSnitchTest.java | 354 +- .../cassandra/locator/ReplicaPlansTest.java | 18 +- .../locator/ReplicationFactorTest.java | 4 +- .../ReplicationStrategyEndpointCacheTest.java | 107 - .../cassandra/locator/SimpleStrategyTest.java | 176 +- .../cassandra/locator/TokenMetadataTest.java | 399 -- ...StrategyTest.java => WithPartitioner.java} | 39 +- .../metrics/TrieMemtableMetricsTest.java | 2 - .../apache/cassandra/net/ConnectionTest.java | 45 +- .../org/apache/cassandra/net/FramingTest.java | 2 + .../apache/cassandra/net/HandshakeTest.java | 26 +- .../net/MessageSerializationPropertyTest.java | 2 + .../org/apache/cassandra/net/MessageTest.java | 15 +- .../cassandra/net/MessagingServiceTest.java | 2 + .../net/MockMessagingServiceTest.java | 4 +- .../net/OutboundConnectionsTest.java | 2 + .../net/OutboundMessageQueueTest.java | 2 + .../net/ProxyHandlerConnectionsTest.java | 2 + ...StartupClusterConnectivityCheckerTest.java | 4 + .../apache/cassandra/repair/FuzzTestBase.java | 41 +- .../cassandra/repair/RepairJobTest.java | 4 + ...epairMessageVerbHandlerOutOfRangeTest.java | 273 ++ .../consistent/CoordinatorMessagingTest.java | 2 +- .../consistent/CoordinatorSessionTest.java | 8 +- .../consistent/CoordinatorSessionsTest.java | 2 +- .../repair/consistent/LocalSessionTest.java | 2 +- .../RepairMessageSerializationsTest.java | 2 + .../repair/messages/RepairMessageTest.java | 6 +- ...ManagerDropKSTest.java => DropKSTest.java} | 2 +- .../schema/MigrationCoordinatorTest.java | 443 -- .../apache/cassandra/schema/MockSchema.java | 166 +- .../schema/RemoveWithoutDroppingTest.java | 125 - .../SchemaChangeDuringRangeMovementTest.java | 254 ++ ...anagerTest.java => SchemaChangesTest.java} | 24 +- .../cassandra/schema/SchemaKeyspaceTest.java | 80 +- .../apache/cassandra/schema/SchemaTest.java | 337 +- .../cassandra/schema/SchemaTestUtil.java | 59 +- .../schema/TableMetadataSerDeTest.java | 187 + .../cassandra/schema/TableMetadataTest.java | 8 + .../service/ActiveRepairServiceTest.java | 92 +- .../service/BootstrapTransientTest.java | 168 +- .../cassandra/service/ClientStateTest.java | 11 +- .../service/DefaultFSErrorHandlerTest.java | 2 + .../service/DiskFailurePolicyTest.java | 2 + .../cassandra/service/JoinTokenRingTest.java | 24 +- .../service/LeaveAndBootstrapTest.java | 739 ---- .../cassandra/service/LegacyAuthFailTest.java | 7 - .../apache/cassandra/service/MoveTest.java | 1102 ----- .../cassandra/service/MoveTransientTest.java | 705 ---- .../service/PartitionDenylistTest.java | 54 +- .../apache/cassandra/service/RemoveTest.java | 92 +- .../cassandra/service/SerializationsTest.java | 3 + .../cassandra/service/StorageProxyTest.java | 18 +- .../service/StorageServiceDrainTest.java | 10 +- .../service/StorageServiceServerM3PTest.java | 12 +- .../service/StorageServiceServerTest.java | 554 ++- .../cassandra/service/StorageServiceTest.java | 133 +- .../service/WriteResponseHandlerTest.java | 18 +- .../WriteResponseHandlerTransientTest.java | 33 +- .../paxos/PaxosVerbHandlerOutOfRangeTest.java | 195 + .../reads/AbstractReadResponseTest.java | 7 +- .../service/reads/DataResolverTest.java | 38 +- .../service/reads/DigestResolverTest.java | 3 +- .../service/reads/ReadExecutorTest.java | 5 +- .../reads/range/ReplicaPlanMergerTest.java | 14 + .../service/reads/range/TokenUpdater.java | 48 +- .../reads/repair/AbstractReadRepairTest.java | 48 +- .../reads/repair/BlockingReadRepairTest.java | 4 +- .../DiagEventsBlockingReadRepairTest.java | 4 +- .../repair/RepairedDataVerifierTest.java | 3 +- ...erTest.java => MetadataSnapshotsTest.java} | 2 +- .../streaming/StreamRateLimiterTest.java | 8 +- .../cassandra/streaming/StreamReaderTest.java | 547 +++ .../StreamSessionOwnedRangesTest.java | 221 + .../streaming/StreamSessionTest.java | 2 +- .../cassandra/tcm/BootWithMetadataTest.java | 184 + .../cassandra/tcm/ClusterMetadataTest.java | 110 + .../ClusterMetadataTransformationTest.java | 316 ++ .../tcm/DiscoverySimulationTest.java | 186 + .../apache/cassandra/tcm/LogStateTest.java | 102 + .../tcm/RecentlySealedPeriodsTest.java | 112 + .../cassandra/tcm/RemoteProcessorTest.java | 110 + .../tcm/compatibility/GossipHelperTest.java | 233 ++ .../ClientNotificationListenerTest.java | 63 + .../tcm/log/DistributedLogStateTest.java | 157 + .../cassandra/tcm/log/LocalLogTest.java | 256 ++ .../tcm/log/LocalStorageLogStateTest.java | 138 + .../tcm/log/LogListenerNotificationTest.java | 138 + .../cassandra/tcm/log/LogStateTestBase.java | 273 ++ .../tcm/membership/MembershipUtils.java | 60 + .../cassandra/tcm/ownership/DeltaMapTest.java | 217 + .../tcm/ownership/OwnershipUtils.java | 242 ++ .../ownership/PrimaryRangeComparatorTest.java | 107 + .../UniformRangePlacementIntegrationTest.java | 100 + .../ownership/UniformRangePlacementTest.java | 320 ++ .../InProgressSequenceCancellationTest.java | 344 ++ .../tcm/sequences/ProgressBarrierTest.java | 351 ++ .../tcm/sequences/SequencesUtils.java | 172 + .../transformations/EventsMetadataTest.java | 191 + .../tcm/transformations/PrepareLeaveTest.java | 177 + .../cassandra/tools/JMXCompatabilityTest.java | 38 +- .../StandaloneSplitterWithCQLTesterTest.java | 20 + .../StandaloneVerifierOnSSTablesTest.java | 3 +- .../cassandra/tools/SystemExitException.java | 3 + .../cassandra/tools/TopPartitionsTest.java | 7 +- .../nodetool/CIDRFilteringStatsTest.java | 3 - .../tools/nodetool/DropCIDRGroupTest.java | 4 - .../nodetool/GetAuthCacheConfigTest.java | 1 - .../tools/nodetool/GetCIDRGroupsOfIPTest.java | 4 - .../tools/nodetool/GossipInfoTest.java | 1 - .../InvalidateCIDRPermissionsCacheTest.java | 3 - .../InvalidateCredentialsCacheTest.java | 4 - .../InvalidateJmxPermissionsCacheTest.java | 19 +- ...InvalidateNetworkPermissionsCacheTest.java | 3 - .../InvalidatePermissionsCacheTest.java | 41 +- .../nodetool/InvalidateRolesCacheTest.java | 3 - .../tools/nodetool/ListCIDRGroupTest.java | 6 +- .../nodetool/ReloadCIDRGroupsCacheTest.java | 3 - .../nodetool/SetAuthCacheConfigTest.java | 1 - .../tools/nodetool/TableHistogramsTest.java | 3 +- .../tools/nodetool/TableStatsTest.java | 4 +- .../tools/nodetool/UpdateCIDRGroupTest.java | 3 - .../cassandra/tools/nodetool/VerifyTest.java | 1 - .../triggers/TriggersSchemaTest.java | 8 +- .../cassandra/triggers/TriggersTest.java | 2 +- .../utils/RecomputingSupplierTest.java | 157 - .../utils/btree/BTreeBiMapGuavaTest.java | 78 + .../cassandra/utils/btree/BTreeBiMapTest.java | 132 + .../utils/btree/BTreeMapGuavaTest.java | 128 + .../cassandra/utils/btree/BTreeMapTest.java | 133 + .../utils/btree/BTreeMultimapTest.java | 118 + .../utils/btree/BTreeSetGuavaTest.java | 121 + .../concurrent/AbstractTransactionalTest.java | 5 +- tools/bin/addtocmstool | 49 + .../io/sstable/StressCQLSSTableWriter.java | 25 +- .../cassandra/stress/CompactionStress.java | 18 +- 934 files changed, 66115 insertions(+), 21599 deletions(-) create mode 100755 ci/harry_simulation.sh create mode 100644 conf/harry-example.yaml create mode 100644 lib/harry-core-0.0.2-CASSANDRA-18768.jar create mode 100644 src/java/org/apache/cassandra/db/AbstractMutationVerbHandler.java create mode 100644 src/java/org/apache/cassandra/db/virtual/ClusterMetadataLogTable.java create mode 100644 src/java/org/apache/cassandra/db/virtual/LocalTable.java create mode 100644 src/java/org/apache/cassandra/db/virtual/PeersTable.java create mode 100644 src/java/org/apache/cassandra/dht/OwnedRanges.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterSyncPendingRanges.java => src/java/org/apache/cassandra/exceptions/CoordinatorBehindException.java (72%) create mode 100644 src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java create mode 100644 src/java/org/apache/cassandra/gms/NewGossiper.java create mode 100644 src/java/org/apache/cassandra/locator/CMSPlacementStrategy.java create mode 100644 src/java/org/apache/cassandra/locator/MetaStrategy.java delete mode 100644 src/java/org/apache/cassandra/locator/PendingRangeMaps.java rename src/java/org/apache/cassandra/locator/{TokenMetadataDiagnostics.java => SystemStrategy.java} (52%) delete mode 100644 src/java/org/apache/cassandra/locator/TokenMetadata.java delete mode 100644 src/java/org/apache/cassandra/locator/TokenMetadataEvent.java create mode 100644 src/java/org/apache/cassandra/metrics/TCMMetrics.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceMarkShutdown.java => src/java/org/apache/cassandra/repair/RepairOutOfTokenRangeException.java (68%) delete mode 100644 src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java create mode 100644 src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java delete mode 100644 src/java/org/apache/cassandra/schema/MigrationCoordinator.java delete mode 100644 src/java/org/apache/cassandra/schema/OfflineSchemaUpdateHandler.java delete mode 100644 src/java/org/apache/cassandra/schema/SchemaUpdateHandler.java delete mode 100644 src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java delete mode 100644 src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java delete mode 100644 src/java/org/apache/cassandra/schema/TableMetadataRefCache.java delete mode 100644 src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java delete mode 100644 src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceDiagnostics.java delete mode 100644 src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceEvent.java delete mode 100644 src/java/org/apache/cassandra/service/RangeRelocator.java create mode 100644 src/java/org/apache/cassandra/service/Rebuild.java create mode 100644 src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java create mode 100644 src/java/org/apache/cassandra/streaming/DataMovement.java create mode 100644 src/java/org/apache/cassandra/streaming/DataMovementVerbHandler.java create mode 100644 src/java/org/apache/cassandra/streaming/StreamReceivedOutOfTokenRangeException.java create mode 100644 src/java/org/apache/cassandra/streaming/StreamRequestOutOfTokenRangeException.java create mode 100644 src/java/org/apache/cassandra/tcm/AbstractLocalProcessor.java create mode 100644 src/java/org/apache/cassandra/tcm/AtomicLongBackedProcessor.java create mode 100644 src/java/org/apache/cassandra/tcm/CMSOperations.java create mode 100644 src/java/org/apache/cassandra/tcm/CMSOperationsMBean.java create mode 100644 src/java/org/apache/cassandra/tcm/ClusterMetadata.java create mode 100644 src/java/org/apache/cassandra/tcm/ClusterMetadataService.java create mode 100644 src/java/org/apache/cassandra/tcm/Commit.java create mode 100644 src/java/org/apache/cassandra/tcm/CurrentEpochRequestHandler.java create mode 100644 src/java/org/apache/cassandra/tcm/Discovery.java create mode 100644 src/java/org/apache/cassandra/tcm/Epoch.java create mode 100644 src/java/org/apache/cassandra/tcm/EpochAwareDebounce.java create mode 100644 src/java/org/apache/cassandra/tcm/FetchCMSLog.java create mode 100644 src/java/org/apache/cassandra/tcm/FetchPeerLog.java create mode 100644 src/java/org/apache/cassandra/tcm/MetadataKey.java create mode 100644 src/java/org/apache/cassandra/tcm/MetadataKeys.java create mode 100644 src/java/org/apache/cassandra/tcm/MetadataSnapshots.java create mode 100644 src/java/org/apache/cassandra/tcm/MetadataValue.java create mode 100644 src/java/org/apache/cassandra/tcm/MultiStepOperation.java rename test/unit/org/apache/cassandra/service/StorageServiceAccessor.java => src/java/org/apache/cassandra/tcm/NotCMSException.java (75%) create mode 100644 src/java/org/apache/cassandra/tcm/PaxosBackedProcessor.java create mode 100644 src/java/org/apache/cassandra/tcm/PeerLogFetcher.java create mode 100644 src/java/org/apache/cassandra/tcm/Period.java create mode 100644 src/java/org/apache/cassandra/tcm/Processor.java create mode 100644 src/java/org/apache/cassandra/tcm/RecentlySealedPeriods.java create mode 100644 src/java/org/apache/cassandra/tcm/RemoteProcessor.java create mode 100644 src/java/org/apache/cassandra/tcm/Retry.java create mode 100644 src/java/org/apache/cassandra/tcm/Sealed.java create mode 100644 src/java/org/apache/cassandra/tcm/Startup.java create mode 100644 src/java/org/apache/cassandra/tcm/StubClusterMetadataService.java create mode 100644 src/java/org/apache/cassandra/tcm/TCM_implementation.md create mode 100644 src/java/org/apache/cassandra/tcm/TransactionalClusterMetadata.md create mode 100644 src/java/org/apache/cassandra/tcm/Transformation.java create mode 100644 src/java/org/apache/cassandra/tcm/compatibility/GossipHelper.java create mode 100644 src/java/org/apache/cassandra/tcm/compatibility/TokenRingUtils.java create mode 100644 src/java/org/apache/cassandra/tcm/extensions/AbstractExtensionValue.java create mode 100644 src/java/org/apache/cassandra/tcm/extensions/EpochValue.java create mode 100644 src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapReplacing.java => src/java/org/apache/cassandra/tcm/extensions/ExtensionValue.java (58%) create mode 100644 src/java/org/apache/cassandra/tcm/extensions/IntValue.java create mode 100644 src/java/org/apache/cassandra/tcm/extensions/StringValue.java rename src/java/org/apache/cassandra/{schema/DefaultSchemaUpdateHandlerFactory.java => tcm/listeners/ChangeListener.java} (54%) create mode 100644 src/java/org/apache/cassandra/tcm/listeners/ClientNotificationListener.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSendShutdownToAll.java => src/java/org/apache/cassandra/tcm/listeners/InitializationListener.java (60%) create mode 100644 src/java/org/apache/cassandra/tcm/listeners/LegacyStateListener.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeft.java => src/java/org/apache/cassandra/tcm/listeners/LogListener.java (69%) create mode 100644 src/java/org/apache/cassandra/tcm/listeners/MetadataSnapshotListener.java create mode 100644 src/java/org/apache/cassandra/tcm/listeners/PlacementsChangeListener.java create mode 100644 src/java/org/apache/cassandra/tcm/listeners/SchemaListener.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetNormal.java => src/java/org/apache/cassandra/tcm/listeners/UpgradeMigrationListener.java (52%) create mode 100644 src/java/org/apache/cassandra/tcm/log/Entry.java create mode 100644 src/java/org/apache/cassandra/tcm/log/LocalLog.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeaving.java => src/java/org/apache/cassandra/tcm/log/LogReader.java (68%) create mode 100644 src/java/org/apache/cassandra/tcm/log/LogState.java create mode 100644 src/java/org/apache/cassandra/tcm/log/LogStorage.java create mode 100644 src/java/org/apache/cassandra/tcm/log/Replication.java create mode 100644 src/java/org/apache/cassandra/tcm/log/SystemKeyspaceStorage.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/Directory.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/Location.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/NodeAddresses.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/NodeId.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/NodeState.java create mode 100644 src/java/org/apache/cassandra/tcm/membership/NodeVersion.java create mode 100644 src/java/org/apache/cassandra/tcm/migration/ClusterMetadataHolder.java create mode 100644 src/java/org/apache/cassandra/tcm/migration/Election.java create mode 100644 src/java/org/apache/cassandra/tcm/migration/GossipCMSListener.java create mode 100644 src/java/org/apache/cassandra/tcm/migration/GossipProcessor.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/DataPlacements.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/Delta.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/EntireRange.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/MovementMap.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/PlacementDeltas.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/PlacementForRange.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/PlacementProvider.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/PlacementTransitionPlan.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/PrimaryRangeComparator.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/ReplicationMap.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/TokenMap.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/UniformRangePlacement.java create mode 100644 src/java/org/apache/cassandra/tcm/ownership/VersionedEndpoints.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/AddToCMS.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/BootstrapAndJoin.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/CancelCMSReconfiguration.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/DataMovements.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/InProgressSequences.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/LeaveStreams.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/LockedRanges.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/Move.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/ProgressBarrier.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/RemoveNodeStreams.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/ReplaceSameAddress.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/SequenceState.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/UnbootstrapAndLeave.java create mode 100644 src/java/org/apache/cassandra/tcm/sequences/UnbootstrapStreams.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/AsymmetricMetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/MessageSerializers.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/MetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/PartitionerAwareMetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/UDTAndFunctionsAwareMetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/UDTAwareMetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/VerboseMetadataSerializer.java create mode 100644 src/java/org/apache/cassandra/tcm/serialization/Version.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/AlterSchema.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/ApplyPlacementDeltas.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/Assassinate.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/CancelInProgressSequence.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/CustomTransformation.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/ForceSnapshot.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/PrepareJoin.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/PrepareMove.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/PrepareReplace.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/Register.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/SealPeriod.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/Startup.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/Unregister.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/UnsafeJoin.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/AdvanceCMSReconfiguration.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/BaseMembershipTransformation.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/FinishAddToCMS.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/Initialize.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/PreInitialize.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/PrepareCMSReconfiguration.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/RemoveFromCMS.java create mode 100644 src/java/org/apache/cassandra/tcm/transformations/cms/StartAddToCMS.java create mode 100644 src/java/org/apache/cassandra/tools/TransformClusterMetadataHelper.java create mode 100644 src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java create mode 100644 src/java/org/apache/cassandra/tools/nodetool/DescribeCMS.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWithAll.java => src/java/org/apache/cassandra/tools/nodetool/InitializeCMS.java (53%) create mode 100644 src/java/org/apache/cassandra/tools/nodetool/ReconfigureCMS.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapping.java => src/java/org/apache/cassandra/tools/nodetool/SealPeriod.java (66%) delete mode 100644 src/java/org/apache/cassandra/utils/RecomputingSupplier.java create mode 100644 src/java/org/apache/cassandra/utils/btree/AbstractBTreeMap.java create mode 100644 src/java/org/apache/cassandra/utils/btree/BTreeBiMap.java create mode 100644 src/java/org/apache/cassandra/utils/btree/BTreeMap.java create mode 100644 src/java/org/apache/cassandra/utils/btree/BTreeMultimap.java create mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java create mode 100644 test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java create mode 100644 test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java create mode 100644 test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java create mode 100644 test/distributed/org/apache/cassandra/distributed/impl/TestChangeListener.java create mode 100644 test/distributed/org/apache/cassandra/distributed/shared/PreventSystemExit.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/CreateTableNonDeterministicTest.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/test/MigrationCoordinatorTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/RemoveNodeTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/BootWithMetadataTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/BounceIndexRebuildTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/BounceResetHostIdTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/CMSMembershipMetricsTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ClusterMetadataTestHelper.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ConflictingAddressRestartTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/DiscoverCMSTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/DistributedLogTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/FetchLogFromPeersTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ForceSnapshotTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/InProgressSequenceCoordinationTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ModelChecker.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/PauseCommitsTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/QuorumIntersectionSimulatorTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ReconfigureCMSTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/RegisterTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/RequestCurrentEpochTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/RngUtils.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/log/TriggeredReconfigureCMSTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/AssignSameTokenToMultipleNodesTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapResetProgressTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/CMSMembershipTest.java rename test/distributed/org/apache/cassandra/distributed/test/ring/{CleanupFailureTest.java => CleanupDuringRangeMovementTest.java} (53%) create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/PendingWritesTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/RangeVersioningTest.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/ReadsDuringBootstrapTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/ring/StopProcessingExceptionTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/tcm/LogReplicationSmokeTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeTest.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/util/byterewrite/StatusChangeListener.java delete mode 100644 test/microbench/org/apache/cassandra/test/microbench/PendingRangesBench.java delete mode 100644 test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterUpdateGossip.java delete mode 100644 test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceBootstrap.java delete mode 100644 test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWith.java delete mode 100644 test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncSchemaForBootstrap.java rename test/simulator/main/org/apache/cassandra/simulator/cluster/{OnInstanceSyncPendingRanges.java => Quiesce.java} (53%) create mode 100644 test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/AlwaysDeliverNetworkScheduler.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/FixedLossNetworkScheduler.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java create mode 100644 test/unit/org/apache/cassandra/cql3/AlterSchemaStatementTest.java create mode 100644 test/unit/org/apache/cassandra/db/CounterMutationVerbHandlerOutOfRangeTest.java create mode 100644 test/unit/org/apache/cassandra/db/MutationVerbHandlerOutOfRangeTest.java create mode 100644 test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java delete mode 100644 test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java create mode 100644 test/unit/org/apache/cassandra/gms/NewGossiperTest.java delete mode 100644 test/unit/org/apache/cassandra/gms/PendingRangeCalculatorServiceTest.java delete mode 100644 test/unit/org/apache/cassandra/gms/ShadowRoundTest.java create mode 100644 test/unit/org/apache/cassandra/locator/MetaStrategyTest.java delete mode 100644 test/unit/org/apache/cassandra/locator/PendingRangeMapsTest.java delete mode 100644 test/unit/org/apache/cassandra/locator/ReplicationStrategyEndpointCacheTest.java delete mode 100644 test/unit/org/apache/cassandra/locator/TokenMetadataTest.java rename test/unit/org/apache/cassandra/locator/{AbstractReplicationStrategyTest.java => WithPartitioner.java} (50%) create mode 100644 test/unit/org/apache/cassandra/repair/RepairMessageVerbHandlerOutOfRangeTest.java rename test/unit/org/apache/cassandra/schema/{MigrationManagerDropKSTest.java => DropKSTest.java} (99%) delete mode 100644 test/unit/org/apache/cassandra/schema/MigrationCoordinatorTest.java delete mode 100644 test/unit/org/apache/cassandra/schema/RemoveWithoutDroppingTest.java create mode 100644 test/unit/org/apache/cassandra/schema/SchemaChangeDuringRangeMovementTest.java rename test/unit/org/apache/cassandra/schema/{MigrationManagerTest.java => SchemaChangesTest.java} (97%) create mode 100644 test/unit/org/apache/cassandra/schema/TableMetadataSerDeTest.java delete mode 100644 test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java delete mode 100644 test/unit/org/apache/cassandra/service/MoveTest.java delete mode 100644 test/unit/org/apache/cassandra/service/MoveTransientTest.java create mode 100644 test/unit/org/apache/cassandra/service/paxos/PaxosVerbHandlerOutOfRangeTest.java rename test/unit/org/apache/cassandra/service/snapshot/{SnapshotManagerTest.java => MetadataSnapshotsTest.java} (99%) create mode 100644 test/unit/org/apache/cassandra/streaming/StreamReaderTest.java create mode 100644 test/unit/org/apache/cassandra/streaming/StreamSessionOwnedRangesTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/BootWithMetadataTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/ClusterMetadataTransformationTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/DiscoverySimulationTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/LogStateTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/RecentlySealedPeriodsTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/RemoteProcessorTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/compatibility/GossipHelperTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/listeners/ClientNotificationListenerTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/log/DistributedLogStateTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/log/LocalLogTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/log/LocalStorageLogStateTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/log/LogListenerNotificationTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/log/LogStateTestBase.java create mode 100644 test/unit/org/apache/cassandra/tcm/membership/MembershipUtils.java create mode 100644 test/unit/org/apache/cassandra/tcm/ownership/DeltaMapTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/ownership/OwnershipUtils.java create mode 100644 test/unit/org/apache/cassandra/tcm/ownership/PrimaryRangeComparatorTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/sequences/InProgressSequenceCancellationTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/sequences/SequencesUtils.java create mode 100644 test/unit/org/apache/cassandra/tcm/transformations/EventsMetadataTest.java create mode 100644 test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java delete mode 100644 test/unit/org/apache/cassandra/utils/RecomputingSupplierTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeBiMapGuavaTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeBiMapTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeMapGuavaTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeMapTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeMultimapTest.java create mode 100644 test/unit/org/apache/cassandra/utils/btree/BTreeSetGuavaTest.java create mode 100755 tools/bin/addtocmstool diff --git a/.build/build-resolver.xml b/.build/build-resolver.xml index c95dc1eb78..889dc11bde 100644 --- a/.build/build-resolver.xml +++ b/.build/build-resolver.xml @@ -59,7 +59,7 @@ - + @@ -267,6 +267,10 @@ + + + + diff --git a/.build/cassandra-build-deps-template.xml b/.build/cassandra-build-deps-template.xml index 357325f8be..b620cda8f5 100644 --- a/.build/cassandra-build-deps-template.xml +++ b/.build/cassandra-build-deps-template.xml @@ -70,6 +70,10 @@ com.puppycrawl.tools checkstyle + + com.google.guava + guava-testlib + org.quicktheories quicktheories diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml index 3991bc5fde..39766937ca 100644 --- a/.build/parent-pom-template.xml +++ b/.build/parent-pom-template.xml @@ -330,6 +330,11 @@ + + com.google.guava + guava-testlib + 27.0-jre + com.google.jimfs jimfs @@ -511,8 +516,9 @@ org.apache.cassandra harry-core - 0.0.1 - test + harry-core-0.0.2-CASSANDRA-18768 + system + ${test.lib}/harry-core-0.0.2-CASSANDRA-18768.jar org.reflections diff --git a/.circleci/config.yml b/.circleci/config.yml index 145afaa557..038c22578d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -75,8 +75,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -111,10 +111,10 @@ jobs: j17_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -182,8 +182,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -246,8 +246,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -281,10 +281,10 @@ jobs: j17_cqlsh_dtests_py311_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -354,8 +354,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -392,7 +392,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -443,8 +443,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -481,7 +481,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -532,8 +532,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -597,8 +597,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -633,10 +633,10 @@ jobs: j17_cqlsh_dtests_py3_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -706,8 +706,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -744,7 +744,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -823,8 +823,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -858,10 +858,10 @@ jobs: j17_cqlsh_dtests_py38_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -931,8 +931,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -966,10 +966,10 @@ jobs: j17_dtests_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1085,8 +1085,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1120,10 +1120,10 @@ jobs: j17_dtests_offheap_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1217,8 +1217,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1252,10 +1252,10 @@ jobs: j11_dtests_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1349,8 +1349,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1388,7 +1388,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1467,8 +1467,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1503,10 +1503,10 @@ jobs: j17_cqlsh_dtests_py3: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -1576,8 +1576,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1614,7 +1614,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1665,8 +1665,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1704,7 +1704,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1783,8 +1783,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1821,7 +1821,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -1872,8 +1872,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -1907,10 +1907,10 @@ jobs: j11_cqlsh_dtests_py311: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -1980,8 +1980,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2016,10 +2016,10 @@ jobs: j17_dtests_large_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2113,8 +2113,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2148,10 +2148,10 @@ jobs: j17_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2267,8 +2267,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2305,7 +2305,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2384,8 +2384,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2420,10 +2420,10 @@ jobs: j17_cqlsh_dtests_py311: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -2493,8 +2493,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2528,10 +2528,10 @@ jobs: j11_cqlsh_dtests_py38: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -2601,8 +2601,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2640,7 +2640,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2691,8 +2691,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2729,7 +2729,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2780,8 +2780,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2819,7 +2819,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -2898,8 +2898,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -2936,7 +2936,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3093,8 +3093,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3129,7 +3129,7 @@ jobs: j11_dtests_large_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: xlarge working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 4 @@ -3178,8 +3178,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3214,10 +3214,10 @@ jobs: j11_dtests_large_vnode_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3311,8 +3311,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3350,7 +3350,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3429,8 +3429,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3464,10 +3464,10 @@ jobs: j11_cqlsh_dtests_py38_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -3537,8 +3537,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3573,7 +3573,7 @@ jobs: j11_dtests_large: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: xlarge working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 4 @@ -3622,8 +3622,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3661,7 +3661,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3712,8 +3712,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3751,7 +3751,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3830,8 +3830,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -3866,10 +3866,10 @@ jobs: j11_upgrade_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: xlarge working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -3963,8 +3963,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4002,7 +4002,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 10 steps: - attach_workspace: at: /home/cassandra @@ -4081,8 +4081,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4119,7 +4119,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4276,8 +4276,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4311,10 +4311,10 @@ jobs: j11_cqlsh_dtests_py3: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -4384,8 +4384,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4491,8 +4491,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4529,7 +4529,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4608,8 +4608,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4647,7 +4647,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4698,8 +4698,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4769,8 +4769,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4805,10 +4805,10 @@ jobs: j11_dtests_offheap_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -4902,8 +4902,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -4941,7 +4941,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 10 steps: - attach_workspace: at: /home/cassandra @@ -5020,8 +5020,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5124,8 +5124,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5196,8 +5196,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5235,7 +5235,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5286,8 +5286,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5399,8 +5399,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5438,7 +5438,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 4 steps: - attach_workspace: at: /home/cassandra @@ -5517,8 +5517,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5589,8 +5589,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5628,7 +5628,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5679,8 +5679,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5715,10 +5715,10 @@ jobs: j17_dtests_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -5786,8 +5786,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5821,10 +5821,10 @@ jobs: j11_dtests_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -5918,8 +5918,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -5957,7 +5957,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6036,8 +6036,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6075,7 +6075,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6126,8 +6126,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6161,10 +6161,10 @@ jobs: j11_cqlsh_dtests_py3_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -6234,8 +6234,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6270,10 +6270,10 @@ jobs: j11_cqlsh_dtests_py311_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -6343,8 +6343,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6382,7 +6382,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6461,8 +6461,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6497,10 +6497,10 @@ jobs: j11_upgrade_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 100 steps: - attach_workspace: at: /home/cassandra @@ -6546,8 +6546,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6582,10 +6582,10 @@ jobs: j11_dtests_large_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6679,8 +6679,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6718,7 +6718,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -6769,8 +6769,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6804,10 +6804,10 @@ jobs: j11_cqlsh_dtests_py3_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -6877,8 +6877,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6949,8 +6949,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -6984,7 +6984,7 @@ jobs: j17_dtests_large_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: xlarge working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 4 @@ -7033,8 +7033,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7068,10 +7068,10 @@ jobs: j11_dtests_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -7117,8 +7117,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7153,10 +7153,10 @@ jobs: j11_cqlsh_dtests_py38_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -7226,8 +7226,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7265,7 +7265,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7316,8 +7316,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7355,7 +7355,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7406,8 +7406,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7445,7 +7445,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7496,8 +7496,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7532,10 +7532,10 @@ jobs: j17_cqlsh_dtests_py38_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -7605,8 +7605,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7668,8 +7668,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7703,10 +7703,10 @@ jobs: j11_cqlsh_dtests_py311_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -7776,8 +7776,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7815,7 +7815,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -7894,8 +7894,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -7929,10 +7929,10 @@ jobs: j17_cqlsh_dtests_py3_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -8002,8 +8002,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8037,10 +8037,10 @@ jobs: j17_cqlsh_dtests_py311_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -8110,8 +8110,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8148,7 +8148,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 10 steps: - attach_workspace: at: /home/cassandra @@ -8227,8 +8227,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8299,8 +8299,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8337,7 +8337,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8388,8 +8388,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8495,8 +8495,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8534,7 +8534,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -8585,8 +8585,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8649,8 +8649,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8685,10 +8685,10 @@ jobs: j11_dtests: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -8734,8 +8734,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8773,7 +8773,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 1 + parallelism: 10 steps: - attach_workspace: at: /home/cassandra @@ -8852,8 +8852,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8887,10 +8887,10 @@ jobs: j17_cqlsh_dtests_py38: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -8960,8 +8960,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -8998,7 +8998,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9077,8 +9077,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9115,7 +9115,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9166,8 +9166,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9201,7 +9201,7 @@ jobs: j17_dtests_large: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: xlarge working_directory: ~/ shell: /bin/bash -eo pipefail -l parallelism: 4 @@ -9250,8 +9250,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9288,7 +9288,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9339,8 +9339,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9411,8 +9411,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9449,7 +9449,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9500,8 +9500,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9535,10 +9535,10 @@ jobs: j11_dtests_vnode: docker: - image: apache/cassandra-testing-ubuntu2004-java11-w-dependencies:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -9584,8 +9584,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9623,7 +9623,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9674,8 +9674,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9709,10 +9709,10 @@ jobs: j17_dtests_offheap: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 50 steps: - attach_workspace: at: /home/cassandra @@ -9780,8 +9780,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9815,10 +9815,10 @@ jobs: j17_dtests_large_repeat: docker: - image: apache/cassandra-testing-ubuntu2004-java11:latest - resource_class: medium + resource_class: large working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -9912,8 +9912,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -9950,7 +9950,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -10001,8 +10001,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false @@ -10039,7 +10039,7 @@ jobs: resource_class: medium working_directory: ~/ shell: /bin/bash -eo pipefail -l - parallelism: 4 + parallelism: 25 steps: - attach_workspace: at: /home/cassandra @@ -10090,8 +10090,8 @@ jobs: - CASS_DRIVER_NO_EXTENSIONS: true - CASS_DRIVER_NO_CYTHON: true - CASSANDRA_SKIP_SYNC: true - - DTEST_REPO: https://github.com/apache/cassandra-dtest.git - - DTEST_BRANCH: trunk + - DTEST_REPO: https://github.com/krummas/cassandra-dtest.git + - DTEST_BRANCH: cep-21-tcm - CCM_MAX_HEAP_SIZE: 1024M - CCM_HEAP_NEWSIZE: 256M - REPEATED_TESTS_STOP_ON_FAILURE: false diff --git a/.gitignore b/.gitignore index aa9e76c932..16cc10a5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ pylib/src/ pylib/cqlshlib/serverversion.py !lib/cassandra-driver-internal-only-*.zip !lib/puresasl-*.zip +!lib/harry-0.0.2-internal-20221121.14211-2.jar # C* debs build-stamp diff --git a/CHANGES.txt b/CHANGES.txt index ba31419cf7..0271f1f87f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Transactional Cluster Metadata [CEP-21] (CASSANDRA-18330) * Add ELAPSED command to cqlsh (CASSANDRA-18861) * Add the ability to disable bulk loading of SSTables (CASSANDRA-18781) * Clean up obsolete functions and simplify cql_version handling in cqlsh (CASSANDRA-18787) diff --git a/NEWS.txt b/NEWS.txt index c6b7e75ee3..37c3c93928 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -71,11 +71,70 @@ using the provided 'sstableupgrade' tool. New features ------------ + [The following is a placeholder, to be revised asap] + - CEP-21 Transactional Cluster Metadata introduces a distributed log for linearizing modifications to cluster + metadata. In the first instance, this encompasses cluster membership, token ownership and schema metadata. See + https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-21%3A+Transactional+Cluster+Metadata for more detail on + the motivation and design, and see "Upgrading" below for specific instructions on migrating clusters to this system. + More updates and documentation to follow. - New Guardrails added: - Whether bulk loading of SSTables is allowed. Upgrading --------- + [The following is a placeholder, to be revised asap] + - Upgrading to 5.1 is currently only supported from 4.0, 4.1 or 5.0. Clusters running earlier releases must first + upgrade to at least the latest 4.0 release. When a cluster is upgraded to 5.1 there is an additional step for + operators to perform. The upgrade is not considered complete until: + 1. All UP nodes are running 5.1 + 2. The Cluster Metadata Service (CMS) has been enabled + The first step is just a regular upgrade, there are no changes to external APIs, SSTable formats to consider. + Step 2 requires an operator to run a new nodetool subcommand, intializecms, on one node in the cluster. + - > nodetool initializecms + Doing so creates the CMS with the local node as its only member. The initializecms command cannot be executed until + step 1 is complete and all nodes are running 5.1 as migrating metadata management over to the new TCM system + requires all nodes to be in agreement over the current state of the cluster. Essentially this means agreement on + schema and topology. Once the upgrade has started, but before initializecms is run, metadata-changing operations are + not permitted and if attempted from an upgraded node will be rejected. + Prohibited operations include: + - schema changes + - node replacement + - bootstrap + - decommission + - move + - assasinate + For the time being there is no mechanism in place to prevent these operations being executed by a node still running + a previous version, though this is planned before release. Any automation which can trigger these operations should + be disabled for the cluster prior to starting the upgrade. + Should any of the prohibited operations be executed (i.e. on a node that is still running a pre-5.1 version) before + the CMS migration, nodes which are DOWN or which have been upgraded will not process the metadata changes. However, + nodes still UP and running the old version will. This will eventually cause the migration to fail, as the cluster + will not be in agreement. + - > nodetool initializecms + Got mismatching cluster metadatas from [/x.x.x.x:7000] aborting migration + See 'nodetool help' or 'nodetool help '. + If the initializecms command fails, it will indicate which nodes’ current metadata does not agree with the node + where the command was executed. To mitigate this situation, bring any mismatching nodes DOWN and rerun the + initializecms command with the additional —ignore flag. + - nodetool intializecms -ignore x.x.x.x + Once the command has run successfully the ignored nodes can be restarted but any metadata changes that they + accepted/and or applied whilst the cluster was in mixed mode will be lost. We plan to improve this before beta, but + in the meantime operators should ensure no schema or membership/ownership changes are performed during the upgrade. + Although the restrictions on metadata-changing operations will be removed as soon as the initial CMS migration is + complete, at that point the CMS will only contain a single member, which is not suitable for real clusters. To + modify the membership of the CMS a second nodetool subcommand, reconfigurecms, should be run. This command allows + the number of required number of CMS members to be specified for each datacenter. Consensus for metadata operations + by the CMS is obtained via Paxos, operating at SERIAL/QUORUM consistency, so the minimum safe size for the CMS is 3 + nodes. In multi-DC deployments, the CMS members may be distributed across DCs to ensure resilience in the case of a + DC outage. It is not recommended to make every node a member of the CMS. Redundancy is the primary concern here, not + locality or latency, so between 3 and 7 nodes per DC depending on the size of the cluster should be the goal. + Deploying to a fresh cluster is more straightforward. As the cluster comes up, at least one node will automatically + nominate itself as the first CMS node. A simple election mechanism ensures that only one node will be chosen if + multiple peers attempt to nominate themselves. As soon as the election is complete, metadata modifications are + supported. Typically, this takes only a few seconds and is likely to complete before all nodes are fully UP. Nodes + which come up during or after an election will learn of the elected first CMS node and direct metadata updates to + it. It is important to remember that at the completion of the election, the CMS still only comprises a single + member. Just as in the upgrade case, operators should add further members as soon as possible. Deprecation ----------- diff --git a/build.xml b/build.xml index 04b742ad51..a3beed1f17 100644 --- a/build.xml +++ b/build.xml @@ -403,7 +403,7 @@ - + diff --git a/ci/harry_simulation.sh b/ci/harry_simulation.sh new file mode 100755 index 0000000000..8af018cecd --- /dev/null +++ b/ci/harry_simulation.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -x +set -o errexit +set -o nounset +set -o pipefail + +rm -f build/test/lib/jars/guava-18.0.jar +current_dir=$(dirname "$(readlink -f "$0")") + +common=(-Dstorage-config=$current_dir/../test/conf + -Djava.awt.headless=true + -ea + -XX:SoftRefLRUPolicyMSPerMB=0 + -XX:HeapDumpPath=build/test + -Dcassandra.test.driver.connection_timeout_ms=10000 + -Dcassandra.test.driver.read_timeout_ms=24000 + -Dcassandra.memtable_row_overhead_computation_step=100 + -Dcassandra.test.use_prepared=true + -Dcassandra.test.sstableformatdevelopment=true + -Djava.security.egd=file:/dev/urandom + -Dcassandra.testtag=.jdk11 + -Dstorage-config=$current_dir/../test/conf + -Djava.awt.headless=true + -Dcassandra.keepBriefBrief=true + -Dcassandra.allow_simplestrategy=true + -Dcassandra.strict.runtime.checks=true + -Dcassandra.reads.thresholds.coordinator.defensive_checks_enabled=true + -Dcassandra.test.flush_local_schema_changes=false + -Dcassandra.test.messagingService.nonGracefulShutdown=true + -Dcassandra.use_nix_recursive_delete=true + -Dcie-cassandra.disable_schema_drop_log=true + -Dcassandra.ring_delay_ms=10000 + -Dcassandra.tolerate_sstable_size=true + -Dcassandra.skip_sync=true + -Dcassandra.debugrefcount=false + -Dcassandra.test.simulator.determinismcheck=strict + -Dcassandra.test.simulator.print_asm=none + -Dlog4j2.disableJmx=true + -Dlog4j2.disable.jmx=true + -Dlog4j.shutdownHookEnabled=false + -Dcassandra.test.logConfigPath=$current_dir/../test/conf/log4j2-dtest-simulator.xml + -Dcassandra.test.logConfigProperty=log4j.configurationFile + -Dlog4j2.configurationFile=$current_dir/../test/conf/log4j2-dtest-simulator.xml + -javaagent:$current_dir/../lib/jamm-0.3.2.jar + -javaagent:$current_dir/../build/test/lib/jars/simulator-asm.jar + -Xbootclasspath/a:$current_dir/../build/test/lib/jars/simulator-bootstrap.jar + -XX:ActiveProcessorCount=4 + -XX:-TieredCompilation + -XX:-BackgroundCompilation + -XX:CICompilerCount=1 + -XX:Tier4CompileThreshold=1000 + -XX:ReservedCodeCacheSize=256M + -Xmx16G + -Xmx4G + -Xss384k + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports java.sql/java.sql=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED + --add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED + -classpath $current_dir/../build/classes/main/:$current_dir/../build/test/classes/:$current_dir/../build/test/lib/jars/*:$current_dir/../build/lib/jars/* + + ) + + +java "${common[@]}" org.apache.cassandra.simulator.test.HarrySimulatorTest diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 4fb3bde86d..1663357bee 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1201,16 +1201,16 @@ range_request_timeout: 10000ms # How long the coordinator should wait for writes to complete. # Lowest acceptable value is 10 ms. # Min unit: ms -write_request_timeout: 2000ms +write_request_timeout: 10000ms # How long the coordinator should wait for counter writes to complete. # Lowest acceptable value is 10 ms. # Min unit: ms -counter_write_request_timeout: 5000ms +counter_write_request_timeout: 1000ms # How long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row. # Lowest acceptable value is 10 ms. # Min unit: ms -cas_contention_timeout: 1000ms +cas_contention_timeout: 5000ms # How long the coordinator should wait for truncates to complete # (This can be much longer, because unless auto_snapshot is disabled # we need to flush first so we can snapshot before removing the data.) @@ -2088,12 +2088,6 @@ drop_compact_storage_enabled: false # enabled: false # ownership_token: "sometoken" # (overriden by "CassandraOwnershipToken" system property) # ownership_filename: ".cassandra_fs_ownership" # (overriden by "cassandra.fs_ownership_filename") -# Prevents a node from starting if snitch's data center differs from previous data center. -# check_dc: -# enabled: true # (overriden by cassandra.ignore_dc system property) -# Prevents a node from starting if snitch's rack differs from previous rack. -# check_rack: -# enabled: true # (overriden by cassandra.ignore_rack system property) # Enable this property to fail startup if the node is down for longer than gc_grace_seconds, to potentially # prevent data resurrection on tables with deletes. By default, this will run against all keyspaces and tables # except the ones specified on excluded_keyspaces and excluded_tables. @@ -2129,4 +2123,4 @@ drop_compact_storage_enabled: false # - Do a rolling restart with all nodes starting with NONE. This sheds the extra cost of checking nodes versions and ensures # a stable cluster. If a node from a previous version was started by accident we won't any longer toggle behaviors as when UPGRADING. # -storage_compatibility_mode: CASSANDRA_4 +storage_compatibility_mode: NONE diff --git a/conf/harry-example.yaml b/conf/harry-example.yaml new file mode 100644 index 0000000000..1868ad9959 --- /dev/null +++ b/conf/harry-example.yaml @@ -0,0 +1,95 @@ +# 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. + +seed: 1596731732524 + +# Default schema provider generates random schema +schema_provider: + fixed: + keyspace: harry + table: test_table + partition_keys: + pk1: bigint + pk2: ascii + clustering_keys: + ck1: ascii + ck2: bigint + regular_columns: + v1: ascii + v2: bigint + v3: ascii + v4: bigint + static_keys: + s1: ascii + s2: bigint + s3: ascii + s4: bigint + +# Clock is a component responsible for mapping _logical_ timestamps to _real-time_ ones. +# +# When reproducing test failures, and for validation purposes, a snapshot of such clock can +# be taken to map a real-time timestamp from the value retrieved from the database in order +# to map it back to the logical timestamp of the operation that wrote this value. +clock: + offset: + offset: 1000 + +drop_schema: false +create_schema: true +truncate_table: true + +# Partition descriptor selector controls how partitions is selected based on the current logical +# timestamp. Default implementation is a sliding window of partition descriptors that will visit +# one partition after the other in the window `slide_after_repeats` times. After that will +# retire one partition descriptor, and pick one instead of it. +partition_descriptor_selector: + default: + window_size: 10 + slide_after_repeats: 100 + +# Clustering descriptor selector controls how clusterings are picked within the partition: +# how many rows there can be in a partition, how many rows will be visited for a logical timestamp, +# how many operations there will be in batch, what kind of operations there will and how often +# each kind of operation is going to occur. +clustering_descriptor_selector: + default: + modifications_per_lts: + type: "constant" + constant: 2 + rows_per_modification: + type: "constant" + constant: 2 + operation_kind_weights: + DELETE_RANGE: 0 + DELETE_SLICE: 0 + DELETE_ROW: 0 + DELETE_COLUMN: 0 + DELETE_PARTITION: 0 + DELETE_COLUMN_WITH_STATICS: 0 + INSERT_WITH_STATICS: 50 + INSERT: 50 + UPDATE_WITH_STATICS: 50 + UPDATE: 50 + column_mask_bitsets: null + max_partition_size: 1000 + +metric_reporter: + no_op: {} + +data_tracker: + locking: + max_seen_lts: -1 + max_complete_lts: -1 diff --git a/lib/harry-core-0.0.2-CASSANDRA-18768.jar b/lib/harry-core-0.0.2-CASSANDRA-18768.jar new file mode 100644 index 0000000000000000000000000000000000000000..292db01f06942d8442204e776b7f25d158c9527b GIT binary patch literal 458194 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²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

  • =|KDIYmOAK(L2#3-|p*8i@nX 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 literal 0 HcmV?d00001 diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 861ebee7e4..252ebabe30 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -34,8 +34,8 @@ class UnexpectedTableStructure(UserWarning): SYSTEM_KEYSPACES = ('system', 'system_schema', 'system_traces', 'system_auth', 'system_distributed', 'system_views', - 'system_virtual_schema') -NONALTERBALE_KEYSPACES = ('system', 'system_schema', 'system_views', 'system_virtual_schema') + 'system_virtual_schema', 'system_cluster_metadata') +NONALTERBALE_KEYSPACES = ('system', 'system_schema', 'system_views', 'system_virtual_schema', 'system_cluster_metadata') class Cql3ParsingRuleSet(CqlParsingRuleSet): diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py index 8e079c8e36..c2a99dc7b5 100644 --- a/pylib/cqlshlib/test/test_cqlsh_completion.py +++ b/pylib/cqlshlib/test/test_cqlsh_completion.py @@ -589,7 +589,7 @@ class TestCqlshCompletion(CqlshCompletionCase): def test_complete_in_drop_type(self): self.trycompletions('DROP TYPE ', choices=['IF', 'system_views.', - 'tags', 'system_traces.', 'system_distributed.', + 'tags', 'system_traces.', 'system_distributed.', 'system_cluster_metadata.', 'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.', 'system_auth.', 'system_virtual_schema.', self.cqlsh.keyspace + '.' ]) @@ -897,7 +897,7 @@ class TestCqlshCompletion(CqlshCompletionCase): self.trycompletions('US', immediate='E ') self.trycompletions('USE ', choices=[self.cqlsh.keyspace, 'system', 'system_auth', 'system_distributed', 'system_schema', 'system_traces', 'system_views', - 'system_virtual_schema' ]) + 'system_virtual_schema', 'system_cluster_metadata' ]) def test_complete_in_create_index(self): self.trycompletions('CREATE I', immediate='NDEX ') @@ -994,6 +994,7 @@ class TestCqlshCompletion(CqlshCompletionCase): 'system_traces.', 'songs', 'system_views.', 'system_virtual_schema.', 'system_schema.', 'system_distributed.', + 'system_cluster_metadata.', self.cqlsh.keyspace + '.']) self.trycompletions('ALTER TABLE IF EXISTS new_table ADD ', choices=['', 'IF']) self.trycompletions('ALTER TABLE IF EXISTS new_table ADD IF NOT EXISTS ', choices=['']) @@ -1021,7 +1022,7 @@ class TestCqlshCompletion(CqlshCompletionCase): self.trycompletions('ALTER TYPE ', choices=['IF', 'system_views.', 'tags', 'system_traces.', 'system_distributed.', 'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.', - 'system_auth.', 'system_virtual_schema.', self.cqlsh.keyspace + '.' + 'system_auth.', 'system_virtual_schema.', 'system_cluster_metadata.', self.cqlsh.keyspace + '.' ]) self.trycompletions('ALTER TYPE IF EXISTS new_type ADD ', choices=['', 'IF']) self.trycompletions('ALTER TYPE IF EXISTS new_type ADD IF NOT EXISTS ', choices=['']) diff --git a/src/java/org/apache/cassandra/auth/AuthKeyspace.java b/src/java/org/apache/cassandra/auth/AuthKeyspace.java index b495c90419..e973f5f455 100644 --- a/src/java/org/apache/cassandra/auth/AuthKeyspace.java +++ b/src/java/org/apache/cassandra/auth/AuthKeyspace.java @@ -41,7 +41,7 @@ public final class AuthKeyspace { } - private static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_AUTH_DEFAULT_RF.getInt(); + public static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_AUTH_DEFAULT_RF.getInt(); /** * Generation is used as a timestamp for automatic table creation on startup. @@ -68,79 +68,85 @@ public final class AuthKeyspace public static final long SUPERUSER_SETUP_DELAY = SUPERUSER_SETUP_DELAY_MS.getLong(); + public static String ROLES_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "role text," + + "is_superuser boolean," + + "can_login boolean," + + "salted_hash text," + + "member_of set," + + "PRIMARY KEY(role))"; private static final TableMetadata Roles = parse(ROLES, "role definitions", - "CREATE TABLE %s (" - + "role text," - + "is_superuser boolean," - + "can_login boolean," - + "salted_hash text," - + "member_of set," - + "PRIMARY KEY(role))"); + ROLES_CQL); + + public static String IDENTITY_TO_ROLES_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "identity text," // opaque identity string for use by role authenticators + + "role text," + + "PRIMARY KEY(identity))"; private static final TableMetadata IdentityToRoles = parse(IDENTITY_TO_ROLES, "mtls authorized identities lookup table", - "CREATE TABLE %s (" - + "identity text," // opaque identity string for use by role authenticators - + "role text," - + "PRIMARY KEY(identity))" + IDENTITY_TO_ROLES_CQL ); + public static String ROLE_MEMBERS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "role text," + + "member text," + + "PRIMARY KEY(role, member))"; private static final TableMetadata RoleMembers = parse(ROLE_MEMBERS, "role memberships lookup table", - "CREATE TABLE %s (" - + "role text," - + "member text," - + "PRIMARY KEY(role, member))"); + ROLE_MEMBERS_CQL); + public static String ROLE_PERMISSIONS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "role text," + + "resource text," + + "permissions set," + + "PRIMARY KEY(role, resource))"; private static final TableMetadata RolePermissions = parse(ROLE_PERMISSIONS, "permissions granted to db roles", - "CREATE TABLE %s (" - + "role text," - + "resource text," - + "permissions set," - + "PRIMARY KEY(role, resource))"); + ROLE_PERMISSIONS_CQL); + public static String RESOURCE_ROLE_INDEX_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "resource text," + + "role text," + + "PRIMARY KEY(resource, role))"; private static final TableMetadata ResourceRoleIndex = parse(RESOURCE_ROLE_INDEX, "index of db roles with permissions granted on a resource", - "CREATE TABLE %s (" - + "resource text," - + "role text," - + "PRIMARY KEY(resource, role))"); + RESOURCE_ROLE_INDEX_CQL); + public static String NETWORK_PERMISSIONS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "role text, " + + "dcs frozen>, " + + "PRIMARY KEY(role))"; private static final TableMetadata NetworkPermissions = parse(NETWORK_PERMISSIONS, "user network permissions", - "CREATE TABLE %s (" - + "role text, " - + "dcs frozen>, " - + "PRIMARY KEY(role))"); + NETWORK_PERMISSIONS_CQL); + + public static String CIDR_PERMISSIONS_CQL = "CREATE TABLE %s (" + + "role text, " + + "cidr_groups frozen>, " + + "PRIMARY KEY(role))"; - public static final String CIDR_PERMISSIONS_TBL_ROLE_COL_NAME = "role"; - public static final String CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME = "cidr_groups"; private static final TableMetadata CIDRPermissions = parse(CIDR_PERMISSIONS, "user cidr permissions", - "CREATE TABLE %s (" - + CIDR_PERMISSIONS_TBL_ROLE_COL_NAME + " text, " - + CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME + " frozen>, " - + "PRIMARY KEY(" + CIDR_PERMISSIONS_TBL_ROLE_COL_NAME + "))" + CIDR_PERMISSIONS_CQL ); - public static final String CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME = "cidr_group"; - public static final String CIDR_GROUPS_TBL_CIDRS_COL_NAME = "cidrs"; + public static String CIDR_GROUPS_CQL = "CREATE TABLE %s (" + + "cidr_group text, " + + "cidrs frozen>>, " + + "PRIMARY KEY(cidr_group))"; private static final TableMetadata CIDRGroups = parse(CIDR_GROUPS, "cidr groups to cidrs mapping", - "CREATE TABLE %s (" - + CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME + " text, " - + CIDR_GROUPS_TBL_CIDRS_COL_NAME + " frozen>>, " - + "PRIMARY KEY(" + CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME + "))" + CIDR_GROUPS_CQL ); private static TableMetadata parse(String name, String description, String cql) @@ -158,7 +164,6 @@ public final class AuthKeyspace KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex, NetworkPermissions, - CIDRPermissions, CIDRGroups, - IdentityToRoles)); + CIDRPermissions, CIDRGroups, IdentityToRoles)); } } diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java index 42ba904b28..925ff39da5 100644 --- a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java +++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java @@ -58,7 +58,7 @@ public class CIDRGroupsMappingLoader UntypedResultSet rows = cidrGroupsMappingManager.getCidrGroupsTableEntries(); for (UntypedResultSet.Row row : rows) { - String cidrGroupName = row.getString(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME); + String cidrGroupName = row.getString("cidr_group"); Set> cidrs = cidrGroupsMappingManager.retrieveCidrsFromRow(row); for (Pair cidr : cidrs) diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java index 6659bdcc15..c3788dfc96 100644 --- a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java +++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java @@ -70,18 +70,15 @@ public class CIDRGroupsMappingManager implements CIDRGroupsMappingManagerMBean if (!MBeanWrapper.instance.isRegistered(MBEAN_NAME)) MBeanWrapper.instance.registerMBean(this, MBEAN_NAME); - String getCidrGroupsQuery = String.format("SELECT %s FROM %s.%s", - AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME, + String getCidrGroupsQuery = String.format("SELECT cidr_group FROM %s.%s", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.CIDR_GROUPS); getCidrGroupsStatement = (SelectStatement) QueryProcessor.getStatement(getCidrGroupsQuery, ClientState.forInternalCalls()); - String getCidrsForCidrGroupQuery = String.format("SELECT %s FROM %s.%s where %s = ?", - AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME, + String getCidrsForCidrGroupQuery = String.format("SELECT cidrs FROM %s.%s where cidr_group = ?", SchemaConstants.AUTH_KEYSPACE_NAME, - AuthKeyspace.CIDR_GROUPS, - AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME); + AuthKeyspace.CIDR_GROUPS); getCidrsForCidrGroupStatement = (SelectStatement) QueryProcessor.getStatement(getCidrsForCidrGroupQuery, ClientState.forInternalCalls()); } @@ -125,11 +122,11 @@ public class CIDRGroupsMappingManager implements CIDRGroupsMappingManagerMBean for (UntypedResultSet.Row row : result) { - if (!row.has(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME)) + if (!row.has("cidr_group")) throw new IllegalStateException("Invalid row " + row + " in table: " + SchemaConstants.AUTH_KEYSPACE_NAME + '.' + AuthKeyspace.CIDR_GROUPS); - availableCidrGroups.add(row.getString(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME)); + availableCidrGroups.add(row.getString("cidr_group")); } return availableCidrGroups; @@ -137,14 +134,13 @@ public class CIDRGroupsMappingManager implements CIDRGroupsMappingManagerMBean public Set> retrieveCidrsFromRow(UntypedResultSet.Row row) { - if (!row.has(AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME)) - throw new RuntimeException("Invalid row, doesn't have column " + - AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME); + if (!row.has("cidrs")) + throw new RuntimeException("Invalid row, doesn't have column cidrs"); Set> cidrs = new HashSet<>(); TupleType tupleType = new TupleType(Arrays.asList(InetAddressType.instance, ShortType.instance)); - Set cidrAsTuples = row.getFrozenSet(AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME, tupleType); + Set cidrAsTuples = row.getFrozenSet("cidrs", tupleType); for (ByteBuffer cidrAsTuple : cidrAsTuples) { ByteBuffer[] splits = tupleType.split(ByteBufferAccessor.instance, cidrAsTuple); @@ -182,12 +178,10 @@ public class CIDRGroupsMappingManager implements CIDRGroupsMappingManagerMBean validCidrs.add(CIDR.getInstance(cidr)); } - String query = String.format("UPDATE %s.%s SET %s = %s WHERE %s = '%s'", + String query = String.format("UPDATE %s.%s SET cidrs = %s WHERE cidr_group = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.CIDR_GROUPS, - AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME, getCidrTuplesSetString(validCidrs), - AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME, cidrGroupName); process(query, CassandraAuthorizer.authWriteConsistencyLevel()); diff --git a/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java b/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java index 459d845bee..9c44c676d3 100644 --- a/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java +++ b/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java @@ -64,11 +64,9 @@ public class CIDRPermissionsManager implements CIDRPermissionsManagerMBean, Auth if (!MBeanWrapper.instance.isRegistered(MBEAN_NAME)) MBeanWrapper.instance.registerMBean(this, MBEAN_NAME); - String getCidrPermissionsOfUserQuery = String.format("SELECT %s FROM %s.%s WHERE %s = ?", - AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, + String getCidrPermissionsOfUserQuery = String.format("SELECT cidr_groups FROM %s.%s WHERE role = ?", SchemaConstants.AUTH_KEYSPACE_NAME, - AuthKeyspace.CIDR_PERMISSIONS, - AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME); + AuthKeyspace.CIDR_PERMISSIONS); getCidrPermissionsOfUserStatement = (SelectStatement) QueryProcessor.getStatement(getCidrPermissionsOfUserQuery, ClientState.forInternalCalls()); } @@ -92,9 +90,9 @@ public class CIDRPermissionsManager implements CIDRPermissionsManagerMBean, Auth ResultMessage.Rows rows = select(getCidrPermissionsOfUserStatement, options); UntypedResultSet result = UntypedResultSet.create(rows.result); - if (!result.isEmpty() && result.one().has(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME)) + if (!result.isEmpty() && result.one().has("cidr_groups")) { - return result.one().getFrozenSet(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, UTF8Type.instance); + return result.one().getFrozenSet("cidr_groups", UTF8Type.instance); } return Collections.emptySet(); @@ -145,12 +143,10 @@ public class CIDRPermissionsManager implements CIDRPermissionsManagerMBean, Auth */ public void setCidrGroupsForRole(RoleResource role, CIDRPermissions cidrPermissions) { - String query = String.format("UPDATE %s.%s SET %s = %s WHERE %s = '%s'", + String query = String.format("UPDATE %s.%s SET cidr_groups = %s WHERE role = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.CIDR_PERMISSIONS, - AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, getCidrPermissionsSetString(cidrPermissions), - AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME, role.getRoleName()); process(query, CassandraAuthorizer.authWriteConsistencyLevel()); @@ -191,18 +187,16 @@ public class CIDRPermissionsManager implements CIDRPermissionsManagerMBean, Auth logger.info("Pre-warming CIDR permissions cache from cidr_permissions table"); Map entries = new HashMap<>(); - UntypedResultSet rows = process(String.format("SELECT %s, %s FROM %s.%s", - AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME, - AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, + UntypedResultSet rows = process(String.format("SELECT role, cidr_groups FROM %s.%s", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.CIDR_PERMISSIONS), CassandraAuthorizer.authReadConsistencyLevel()); for (UntypedResultSet.Row row : rows) { - RoleResource role = RoleResource.role(row.getString(AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME)); + RoleResource role = RoleResource.role(row.getString("role")); CIDRPermissions.Builder builder = new CIDRPermissions.Builder(); - Set cidrGroups = row.getFrozenSet(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, + Set cidrGroups = row.getFrozenSet("cidr_groups", UTF8Type.instance); for (String cidrGroup : cidrGroups) builder.add(cidrGroup); diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java index 3221c85184..921a45dcb2 100644 --- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java +++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java @@ -36,18 +36,19 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.statements.SelectStatement; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.exceptions.*; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.StorageProxy; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.FBUtilities; import org.mindrot.jbcrypt.BCrypt; import static org.apache.cassandra.config.CassandraRelevantProperties.AUTH_BCRYPT_GENSALT_LOG2_ROUNDS; @@ -144,14 +145,21 @@ public class CassandraRoleManager implements IRoleManager } @Override - public void setup() + public void setup(boolean asyncRoleSetup) { loadRoleStatement(); loadIdentityStatement(); - scheduleSetupTask(() -> { + if (asyncRoleSetup) + { + scheduleSetupTask(() -> { + setupDefaultRole(); + return null; + }); + } + else + { setupDefaultRole(); - return null; - }); + } } @Override @@ -417,7 +425,7 @@ public class CassandraRoleManager implements IRoleManager */ private static void setupDefaultRole() { - if (StorageService.instance.getTokenMetadata().sortedTokens().isEmpty()) + if (ClusterMetadata.current().tokenMap.tokens().isEmpty()) throw new IllegalStateException("CassandraRoleManager skipped default role setup: no known tokens in ring"); try @@ -461,7 +469,7 @@ public class CassandraRoleManager implements IRoleManager { // The delay is to give the node a chance to see its peers before attempting the operation ScheduledExecutors.optionalTasks.scheduleSelfRecurring(() -> { - if (!StorageProxy.isSafeToPerformRead()) + if (!StorageProxy.hasJoined()) { logger.trace("Setup task may not run due to it not being safe to perform reads... rescheduling"); scheduleSetupTask(setupTask); @@ -487,7 +495,7 @@ public class CassandraRoleManager implements IRoleManager } catch (RequestValidationException e) { - throw new AssertionError(e); // not supposed to happen + throw new AssertionError(e + " " + FBUtilities.getJustLocalAddress()); // not supposed to happen } } diff --git a/src/java/org/apache/cassandra/auth/DCPermissions.java b/src/java/org/apache/cassandra/auth/DCPermissions.java index d04242dd7e..c8a7716f45 100644 --- a/src/java/org/apache/cassandra/auth/DCPermissions.java +++ b/src/java/org/apache/cassandra/auth/DCPermissions.java @@ -28,6 +28,7 @@ import com.google.common.collect.Sets; import org.apache.cassandra.dht.Datacenters; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.tcm.ClusterMetadata; public abstract class DCPermissions { @@ -93,7 +94,7 @@ public abstract class DCPermissions public void validate() { - Set unknownDcs = Sets.difference(subset, Datacenters.getValidDatacenters()); + Set unknownDcs = Sets.difference(subset, Datacenters.getValidDatacenters(ClusterMetadata.current())); if (!unknownDcs.isEmpty()) { throw new InvalidRequestException(String.format("Invalid value(s) for DATACENTERS '%s'," + diff --git a/src/java/org/apache/cassandra/auth/IRoleManager.java b/src/java/org/apache/cassandra/auth/IRoleManager.java index 460bf1a16f..0479913846 100644 --- a/src/java/org/apache/cassandra/auth/IRoleManager.java +++ b/src/java/org/apache/cassandra/auth/IRoleManager.java @@ -22,6 +22,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestValidationException; @@ -226,7 +228,16 @@ public interface IRoleManager extends AuthCache.BulkLoader 0) { int endpointThrottleInKiB = throttleInKB / endpointsCount; @@ -471,29 +473,27 @@ public class BatchlogManager implements BatchlogManagerMBean Set hintedNodes) { String ks = mutation.getKeyspaceName(); - Keyspace keyspace = Keyspace.open(ks); Token tk = mutation.key().getToken(); + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(ks); // TODO: this logic could do with revisiting at some point, as it is unclear what its rationale is // we perform a local write, ignoring errors and inline in this thread (potentially slowing replay down) // effectively bumping CL for locally owned writes and also potentially stalling log replay if an error occurs // once we decide how it should work, it can also probably be simplified, and avoid constructing a ReplicaPlan directly - ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(keyspace, tk); - Replicas.temporaryAssertFull(liveAndDown.all()); // TODO in CASSANDRA-14549 + ReplicaLayout.ForTokenWrite allReplias = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspaceMetadata, tk); + ReplicaPlan.ForWrite replicaPlan = forReplayMutation(metadata, Keyspace.open(ks), tk); - Replica selfReplica = liveAndDown.all().selfIfPresent(); + Replica selfReplica = allReplias.all().selfIfPresent(); if (selfReplica != null) mutation.apply(); - ReplicaLayout.ForTokenWrite liveRemoteOnly = liveAndDown.filter( - r -> FailureDetector.isReplicaAlive.test(r) && r != selfReplica); - - for (Replica replica : liveAndDown.all()) + for (Replica replica : allReplias.all()) { - if (replica == selfReplica || liveRemoteOnly.all().contains(replica)) + if (replica == selfReplica || replicaPlan.liveAndDown().contains(replica)) continue; - UUID hostId = StorageService.instance.getHostIdForEndpoint(replica.endpoint()); + UUID hostId = metadata.directory.peerId(replica.endpoint()).toUUID(); if (null != hostId) { HintsService.instance.write(hostId, Hint.create(mutation, writtenAt)); @@ -501,15 +501,26 @@ public class BatchlogManager implements BatchlogManagerMBean } } - ReplicaPlan.ForWrite replicaPlan = new ReplicaPlan.ForWrite(keyspace, liveAndDown.replicationStrategy(), - ConsistencyLevel.ONE, liveRemoteOnly.pending(), liveRemoteOnly.all(), liveRemoteOnly.all(), liveRemoteOnly.all()); ReplayWriteResponseHandler handler = new ReplayWriteResponseHandler<>(replicaPlan, mutation, nanoTime()); Message message = Message.outWithFlag(MUTATION_REQ, mutation, MessageFlag.CALL_BACK_ON_FAILURE); - for (Replica replica : liveRemoteOnly.all()) + for (Replica replica : replicaPlan.liveAndDown()) MessagingService.instance().sendWriteWithCallback(message, replica, handler); return handler; } + public static ReplicaPlan.ForWrite forReplayMutation(ClusterMetadata metadata, Keyspace keyspace, Token token) + { + ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace.getMetadata(), token); + Replicas.temporaryAssertFull(liveAndDown.all()); // TODO in CASSANDRA-14549 + + Replica selfReplica = liveAndDown.all().selfIfPresent(); + ReplicaLayout.ForTokenWrite liveRemoteOnly = liveAndDown.filter(r -> FailureDetector.isReplicaAlive.test(r) && r != selfReplica); + + return new ReplicaPlan.ForWrite(keyspace, liveAndDown.replicationStrategy(), + ConsistencyLevel.ONE, liveRemoteOnly.pending(), liveRemoteOnly.all(), liveRemoteOnly.all(), liveRemoteOnly.all(), + (cm) -> forReplayMutation(cm, keyspace, token), + metadata.epoch); + } private static int gcgs(Collection mutations) { int gcgs = Integer.MAX_VALUE; diff --git a/src/java/org/apache/cassandra/cache/AutoSavingCache.java b/src/java/org/apache/cassandra/cache/AutoSavingCache.java index 3826eb412e..0cfea0817d 100644 --- a/src/java/org/apache/cassandra/cache/AutoSavingCache.java +++ b/src/java/org/apache/cassandra/cache/AutoSavingCache.java @@ -60,6 +60,7 @@ import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.CacheService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Future; @@ -219,12 +220,13 @@ public class AutoSavingCache extends InstrumentingCache>> futures = new ArrayDeque<>(); long loadByNanos = start + TimeUnit.SECONDS.toNanos(DatabaseDescriptor.getCacheLoadTimeout()); diff --git a/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java b/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java index 9816649424..e732729299 100644 --- a/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java +++ b/src/java/org/apache/cassandra/concurrent/NamedThreadFactory.java @@ -17,12 +17,14 @@ */ package org.apache.cassandra.concurrent; +import java.util.Arrays; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.annotations.VisibleForTesting; import io.netty.util.concurrent.FastThreadLocalThread; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.utils.JVMStabilityInspector; /** @@ -33,6 +35,8 @@ import org.apache.cassandra.utils.JVMStabilityInspector; public class NamedThreadFactory implements ThreadFactory { + public static final Boolean PRESERVE_THREAD_CREATION_STACKTRACE = CassandraRelevantProperties.TEST_PRESERVE_THREAD_CREATION_STACKTRACE.getBoolean(); + private static final AtomicInteger anonymousCounter = new AtomicInteger(); private static volatile String globalPrefix; @@ -159,11 +163,43 @@ public class NamedThreadFactory implements ThreadFactory public static Thread createThread(ThreadGroup threadGroup, Runnable runnable, String name, boolean daemon) { String prefix = globalPrefix; - Thread thread = new FastThreadLocalThread(threadGroup, runnable, prefix != null ? prefix + name : name); + Thread thread; + String threadName = prefix != null ? prefix + name : name; + if (PRESERVE_THREAD_CREATION_STACKTRACE) + thread = new InspectableFastThreadLocalThread(threadGroup, runnable, threadName); + else + thread = new FastThreadLocalThread(threadGroup, runnable, threadName); thread.setDaemon(daemon); return thread; } + public static class InspectableFastThreadLocalThread extends FastThreadLocalThread + { + public StackTraceElement[] creationTrace; + + private void setStack() + { + creationTrace = Thread.currentThread().getStackTrace(); + creationTrace = Arrays.copyOfRange(creationTrace, 2, creationTrace.length); + } + + public InspectableFastThreadLocalThread() { super(); setStack(); } + + public InspectableFastThreadLocalThread(Runnable target) { super(target); setStack(); } + + public InspectableFastThreadLocalThread(ThreadGroup group, Runnable target) { super(group, target); setStack(); } + + public InspectableFastThreadLocalThread(String name) { super(name); setStack(); } + + public InspectableFastThreadLocalThread(ThreadGroup group, String name) { super(group, name); setStack(); } + + public InspectableFastThreadLocalThread(Runnable target, String name) { super(target, name); setStack(); } + + public InspectableFastThreadLocalThread(ThreadGroup group, Runnable target, String name) { super(group, target, name); setStack(); } + + public InspectableFastThreadLocalThread(ThreadGroup group, Runnable target, String name, long stackSize) { super(group, target, name, stackSize); setStack(); } + + } public static T setupThread(T thread, int priority, ClassLoader contextClassLoader, Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { thread.setPriority(priority); diff --git a/src/java/org/apache/cassandra/concurrent/Stage.java b/src/java/org/apache/cassandra/concurrent/Stage.java index 05b6c612fa..4def177422 100644 --- a/src/java/org/apache/cassandra/concurrent/Stage.java +++ b/src/java/org/apache/cassandra/concurrent/Stage.java @@ -55,6 +55,8 @@ public enum Stage INTERNAL_RESPONSE (false, "InternalResponseStage", "internal", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedStage), IMMEDIATE (false, "ImmediateStage", "internal", () -> 0, null, Stage::immediateExecutor), PAXOS_REPAIR (false, "PaxosRepairStage", "internal", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedStage), + INTERNAL_METADATA (false, "InternalMetadataStage", "internal", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedStage), + FETCH_LOG (false, "MetadataFetchLogStage", "internal", () -> 1, null, Stage::singleThreadedStage) ; public final String jmxName; diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index a850e99342..a38da0e22f 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -353,7 +353,7 @@ public enum CassandraRelevantProperties MEMTABLE_OVERHEAD_SIZE("cassandra.memtable.row_overhead_size", "-1"), MEMTABLE_SHARD_COUNT("cassandra.memtable.shard.count"), MEMTABLE_TRIE_SIZE_LIMIT("cassandra.trie_size_limit_mb"), - MIGRATION_DELAY("cassandra.migration_delay_ms", "60000"), + METRICS_REPORTER_CONFIG_FILE("cassandra.metricsReporterConfigFile"), /** Defines the maximum number of unique timed out queries that will be reported in the logs. Use a negative number to remove any limit. */ MONITORING_MAX_OPERATIONS("cassandra.monitoring_max_operations", "50"), /** Defines the interval for reporting any operations that have timed out. */ @@ -468,7 +468,6 @@ public enum CassandraRelevantProperties */ SAI_VECTOR_SEARCH_ORDER_CHUNK_SIZE("cassandra.sai.vector_search.order_chunk_size", "100000"), - SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"), SCHEMA_UPDATE_HANDLER_FACTORY_CLASS("cassandra.schema.update_handler_factory.class"), SEARCH_CONCURRENCY_FACTOR("cassandra.search_concurrency_factor", "1"), @@ -482,6 +481,7 @@ public enum CassandraRelevantProperties SET_SEP_THREAD_NAME("cassandra.set_sep_thread_name", "true"), SHUTDOWN_ANNOUNCE_DELAY_IN_MS("cassandra.shutdown_announce_in_ms", "2000"), SIZE_RECORDER_INTERVAL("cassandra.size_recorder_interval", "300"), + SKIP_GC_INSPECTOR("cassandra.skip_gc_inspector", "false"), SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE("cassandra.skip_paxos_repair_on_topology_change"), /** If necessary for operational purposes, permit certain keyspaces to be ignored for paxos topology repairs. */ SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE_KEYSPACES("cassandra.skip_paxos_repair_on_topology_change_keyspaces"), @@ -518,6 +518,30 @@ public enum CassandraRelevantProperties SYSTEM_AUTH_DEFAULT_RF("cassandra.system_auth.default_rf", "1"), SYSTEM_DISTRIBUTED_DEFAULT_RF("cassandra.system_distributed.default_rf", "3"), SYSTEM_TRACES_DEFAULT_RF("cassandra.system_traces.default_rf", "2"), + + // transactional cluster metadata relevant properties + // TODO: not a fan of being forced to prefix these to satisfy the alphabetic ordering constraint + // but it makes sense to group logically related properties together + + TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES("cassandra.allow_transformations_during_upgrades", "false"), + /** + * for obtaining acknowlegement from peers to make progress in multi-step operations + */ + TCM_PROGRESS_BARRIER_BACKOFF_MILLIS("cassandra.progress_barrier_backoff_ms", "1000"), + TCM_PROGRESS_BARRIER_TIMEOUT_MILLIS("cassandra.progress_barrier_timeout_ms", "3600000"), + /** + * size of in-memory index of max epoch -> sealed period + */ + TCM_RECENTLY_SEALED_PERIOD_INDEX_SIZE("cassandra.recently_sealed_period_index_size", "10"), + + /** + * should replica groups in data placements be sorted to ensure the primary replica is first in the list + */ + TCM_SORT_REPLICA_GROUPS("cassandra.sorted_replica_groups_enabled", "true"), + TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA("cassandra.unsafe_boot_with_clustermetadata", null), + TCM_USE_ATOMIC_LONG_PROCESSOR("cassandra.test.use_atomic_long_processor", "false"), + TCM_USE_NO_OP_REPLICATOR("cassandra.test.use_no_op_replicator", "false"), + TEST_BBFAILHELPER_ENABLED("test.bbfailhelper.enabled"), TEST_BLOB_SHARED_SEED("cassandra.test.blob.shared.seed"), TEST_BYTEMAN_TRANSFORMATIONS_DEBUG("cassandra.test.byteman.transformations.debug"), @@ -543,13 +567,16 @@ public enum CassandraRelevantProperties TEST_IGNORE_SIGAR("cassandra.test.ignore_sigar"), TEST_INVALID_LEGACY_SSTABLE_ROOT("invalid-legacy-sstable-root"), TEST_JVM_DTEST_DISABLE_SSL("cassandra.test.disable_ssl"), + TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY("cassandra.test.messagingService.gracefulShutdown", "false"), TEST_LEGACY_SSTABLE_ROOT("legacy-sstable-root"), TEST_ORG_CAFFINITAS_OHC_SEGMENTCOUNT("org.caffinitas.ohc.segmentCount"), + TEST_PRESERVE_THREAD_CREATION_STACKTRACE("cassandra.test.preserve_thread_creation_stacktrace", "false"), TEST_RANDOM_SEED("cassandra.test.random.seed"), TEST_READ_ITERATION_DELAY_MS("cassandra.test.read_iteration_delay_ms", "0"), TEST_REUSE_PREPARED("cassandra.test.reuse_prepared", "true"), TEST_ROW_CACHE_SIZE("cassandra.test.row_cache_size"), TEST_SERIALIZATION_WRITES("cassandra.test-serialization-writes"), + TEST_SIGAR_NATIVE_LOGGING("sigar.nativeLogging", "true"), TEST_SIMULATOR_DEBUG("cassandra.test.simulator.debug"), TEST_SIMULATOR_DETERMINISM_CHECK("cassandra.test.simulator.determinismcheck", "none"), TEST_SIMULATOR_LIVENESS_CHECK("cassandra.test.simulator.livenesscheck", "true"), @@ -590,7 +617,9 @@ public enum CassandraRelevantProperties USE_NIX_RECURSIVE_DELETE("cassandra.use_nix_recursive_delete"), /** Gossiper compute expiration timeout. Default value 3 days. */ VERY_LONG_TIME_MS("cassandra.very_long_time_ms", "259200000"), - WAIT_FOR_TRACING_EVENTS_TIMEOUT_SECS("cassandra.wait_for_tracing_events_timeout_secs", "0"); + WAIT_FOR_TRACING_EVENTS_TIMEOUT_SECS("cassandra.wait_for_tracing_events_timeout_secs", "0"), + ; + static { diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 6fdcdd5a49..a898ec5cc6 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -171,6 +171,15 @@ public class Config public volatile DurationSpec.LongMillisecondsBound stream_transfer_task_timeout = new DurationSpec.LongMillisecondsBound("12h"); + public volatile DurationSpec.LongMillisecondsBound cms_await_timeout = new DurationSpec.LongMillisecondsBound("120000ms"); + public volatile int cms_default_max_retries = 10; + public volatile DurationSpec.IntMillisecondsBound cms_default_retry_backoff = new DurationSpec.IntMillisecondsBound("50ms"); + /** + * How often we should snapshot the cluster metadata. + */ + public volatile int metadata_snapshot_frequency = 100; + + public volatile double phi_convict_threshold = 8.0; public int concurrent_reads = 32; @@ -1243,5 +1252,21 @@ public class Config public double severity_during_decommission = 0; - public StorageCompatibilityMode storage_compatibility_mode = StorageCompatibilityMode.CASSANDRA_4; + // TODO Revisit MessagingService::current_version + public StorageCompatibilityMode storage_compatibility_mode = StorageCompatibilityMode.NONE; + + /** + * For the purposes of progress barrier we only support ALL, EACH_QUORUM, QUORUM, LOCAL_QUORUM, ANY, and ONE. + * + * We will still try all consistency levels above the lowest acceptable, and only fall back to it if we can not + * collect enough nodes. + */ + public volatile ConsistencyLevel progress_barrier_min_consistency_level = ConsistencyLevel.EACH_QUORUM; + public volatile boolean log_out_of_token_range_requests = true; + public volatile boolean reject_out_of_token_range_requests = true; + public volatile ConsistencyLevel progress_barrier_default_consistency_level = ConsistencyLevel.EACH_QUORUM; + + public volatile DurationSpec.LongMillisecondsBound progress_barrier_timeout = new DurationSpec.LongMillisecondsBound("3600000ms"); + public volatile DurationSpec.LongMillisecondsBound progress_barrier_backoff = new DurationSpec.LongMillisecondsBound("1000ms"); + public boolean unsafe_tcm_mode = false; } diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index ddf9c0d872..9d0cdc61e8 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -29,10 +29,12 @@ import java.net.UnknownHostException; import java.nio.file.FileStore; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -84,6 +86,7 @@ import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.fql.FullQueryLoggerOptions; import org.apache.cassandra.gms.IFailureDetector; +import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.sstable.format.big.BigFormat; @@ -104,6 +107,7 @@ import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.security.JREProvider; import org.apache.cassandra.security.SSLFactory; import org.apache.cassandra.service.CacheService.CacheType; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.paxos.Paxos; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.StorageCompatibilityMode; @@ -135,6 +139,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYS import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.BYTES_PER_SECOND; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.MEBIBYTES; +import static org.apache.cassandra.db.ConsistencyLevel.*; import static org.apache.cassandra.io.util.FileUtils.ONE_GIB; import static org.apache.cassandra.io.util.FileUtils.ONE_MIB; import static org.apache.cassandra.utils.Clock.Global.logInitializationOutcome; @@ -592,12 +597,12 @@ public class DatabaseDescriptor if (conf.repair_session_space.toMebibytes() < 1) throw new ConfigurationException("repair_session_space must be > 0, but was " + conf.repair_session_space); else if (conf.repair_session_space.toMebibytes() > (int) (Runtime.getRuntime().maxMemory() / (4 * 1048576))) - logger.warn("A repair_session_space of " + conf.repair_session_space+ " mebibytes is likely to cause heap pressure"); + logger.warn("A repair_session_space of " + conf.repair_session_space + " mebibytes is likely to cause heap pressure"); checkForLowestAcceptedTimeouts(conf); long valueInBytes = conf.native_transport_max_frame_size.toBytes(); - if (valueInBytes < 0 || valueInBytes > Integer.MAX_VALUE-1) + if (valueInBytes < 0 || valueInBytes > Integer.MAX_VALUE - 1) { throw new ConfigurationException(String.format("native_transport_max_frame_size must be positive value < %dB, but was %dB", Integer.MAX_VALUE, @@ -770,8 +775,8 @@ public class DatabaseDescriptor { // if prepared_statements_cache_size option was set to "auto" then size of the cache should be "max(1/256 of Heap (in MiB), 10MiB)" preparedStatementsCacheSizeInMiB = (conf.prepared_statements_cache_size == null) - ? Math.max(10, (int) (Runtime.getRuntime().maxMemory() / 1024 / 1024 / 256)) - : conf.prepared_statements_cache_size.toMebibytes(); + ? Math.max(10, (int) (Runtime.getRuntime().maxMemory() / 1024 / 1024 / 256)) + : conf.prepared_statements_cache_size.toMebibytes(); if (preparedStatementsCacheSizeInMiB == 0) throw new NumberFormatException(); // to escape duplicating error message @@ -789,8 +794,8 @@ public class DatabaseDescriptor { // if key_cache_size option was set to "auto" then size of the cache should be "min(5% of Heap (in MiB), 100MiB) keyCacheSizeInMiB = (conf.key_cache_size == null) - ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)), 100) - : conf.key_cache_size.toMebibytes(); + ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)), 100) + : conf.key_cache_size.toMebibytes(); if (keyCacheSizeInMiB < 0) throw new NumberFormatException(); // to escape duplicating error message @@ -808,8 +813,8 @@ public class DatabaseDescriptor { // if counter_cache_size option was set to "auto" then size of the cache should be "min(2.5% of Heap (in MiB), 50MiB) counterCacheSizeInMiB = (conf.counter_cache_size == null) - ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.025 / 1024 / 1024)), 50) - : conf.counter_cache_size.toMebibytes(); + ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.025 / 1024 / 1024)), 50) + : conf.counter_cache_size.toMebibytes(); if (counterCacheSizeInMiB < 0) throw new NumberFormatException(); // to escape duplicating error message @@ -817,15 +822,15 @@ public class DatabaseDescriptor catch (NumberFormatException e) { throw new ConfigurationException("counter_cache_size option was set incorrectly to '" - + (conf.counter_cache_size !=null ?conf.counter_cache_size.toString() : null) + "', supported values are >= 0.", false); + + (conf.counter_cache_size != null ? conf.counter_cache_size.toString() : null) + "', supported values are >= 0.", false); } try { // if paxosCacheSizeInMiB option was set to "auto" then size of the cache should be "min(1% of Heap (in MB), 50MB) paxosCacheSizeInMiB = (conf.paxos_cache_size == null) - ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.01 / 1024 / 1024)), 50) - : conf.paxos_cache_size.toMebibytes(); + ? Math.min(Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.01 / 1024 / 1024)), 50) + : conf.paxos_cache_size.toMebibytes(); if (paxosCacheSizeInMiB < 0) throw new NumberFormatException(); // to escape duplicating error message @@ -833,7 +838,7 @@ public class DatabaseDescriptor catch (NumberFormatException e) { throw new ConfigurationException("paxos_cache_size option was set incorrectly to '" - + conf.paxos_cache_size + "', supported values are >= 0.", false); + + conf.paxos_cache_size + "', supported values are >= 0.", false); } // we need this assignment for the Settings virtual table - CASSANDRA-17735 @@ -841,8 +846,8 @@ public class DatabaseDescriptor // if set to empty/"auto" then use 5% of Heap size indexSummaryCapacityInMiB = (conf.index_summary_capacity == null) - ? Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)) - : conf.index_summary_capacity.toMebibytes(); + ? Math.max(1, (int) (Runtime.getRuntime().totalMemory() * 0.05 / 1024 / 1024)) + : conf.index_summary_capacity.toMebibytes(); if (indexSummaryCapacityInMiB < 0) throw new ConfigurationException("index_summary_capacity option was set incorrectly to '" @@ -860,7 +865,7 @@ public class DatabaseDescriptor if (conf.allow_extra_insecure_udfs) logger.warn("Allowing java.lang.System.* access in UDFs is dangerous and not recommended. Set allow_extra_insecure_udfs: false to disable."); - if(conf.scripted_user_defined_functions_enabled) + if (conf.scripted_user_defined_functions_enabled) throw new ConfigurationException("JavaScript user-defined functions were removed in CASSANDRA-18252. " + "Hooks are planned to be introduced as part of CASSANDRA-17280"); @@ -896,7 +901,7 @@ public class DatabaseDescriptor throw new ConfigurationException("max_value_size must be positive", false); else if (conf.max_value_size.toMebibytes() >= 2048) throw new ConfigurationException("max_value_size must be smaller than 2048, but was " - + conf.max_value_size.toString(), false); + + conf.max_value_size.toString(), false); switch (conf.disk_optimization_strategy) { @@ -968,6 +973,20 @@ public class DatabaseDescriptor throw new ConfigurationException(String.format("Invalid configuration. Heap dump is enabled but cannot create heap dump output path: %s.", conf.heap_dump_path != null ? conf.heap_dump_path : "null")); conf.sai_options.validate(); + + List progressBarrierCLsArr = Arrays.asList(ALL, EACH_QUORUM, LOCAL_QUORUM, QUORUM, ONE, NODE_LOCAL); + Set progressBarrierCls = new HashSet<>(progressBarrierCLsArr); + if (!progressBarrierCls.contains(conf.progress_barrier_min_consistency_level)) + { + throw new ConfigurationException(String.format("Invalid value for progress_barrier_min_consistency_level %s. Allowed values: %s", + conf.progress_barrier_min_consistency_level, progressBarrierCLsArr)); + } + + if (!progressBarrierCls.contains(conf.progress_barrier_default_consistency_level)) + { + throw new ConfigurationException(String.format("Invalid value for.progress_barrier_default_consistency_level %s. Allowed values: %s", + conf.progress_barrier_default_consistency_level, progressBarrierCLsArr)); + } } @VisibleForTesting @@ -1264,7 +1283,7 @@ public class DatabaseDescriptor try { Class seedProviderClass = Class.forName(conf.seed_provider.class_name); - seedProvider = (SeedProvider)seedProviderClass.getConstructor(Map.class).newInstance(conf.seed_provider.parameters); + seedProvider = (SeedProvider) seedProviderClass.getConstructor(Map.class).newInstance(conf.seed_provider.parameters); } // there are about 5 checked exceptions that could be thrown here. catch (Exception e) @@ -1278,42 +1297,42 @@ public class DatabaseDescriptor @VisibleForTesting static void checkForLowestAcceptedTimeouts(Config conf) { - if(conf.read_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.read_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("read_request_timeout", conf.read_request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.read_request_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.range_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.range_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("range_request_timeout", conf.range_request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.range_request_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("request_timeout", conf.request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.request_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.write_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.write_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("write_request_timeout", conf.write_request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.write_request_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.cas_contention_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.cas_contention_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("cas_contention_timeout", conf.cas_contention_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.cas_contention_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.counter_write_request_timeout.toMilliseconds()< LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.counter_write_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("counter_write_request_timeout", conf.counter_write_request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.counter_write_request_timeout = new DurationSpec.LongMillisecondsBound("10ms"); } - if(conf.truncate_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) + if (conf.truncate_request_timeout.toMilliseconds() < LOWEST_ACCEPTED_TIMEOUT.toMilliseconds()) { logInfo("truncate_request_timeout", conf.truncate_request_timeout, LOWEST_ACCEPTED_TIMEOUT); conf.truncate_request_timeout = LOWEST_ACCEPTED_TIMEOUT; @@ -1518,7 +1537,9 @@ public class DatabaseDescriptor private static long tryGetSpace(String dir, PathUtils.IOToLongFunction getSpace) { - return PathUtils.tryGetSpace(new File(dir).toPath(), getSpace, e -> { throw new ConfigurationException("Unable check disk space in '" + dir + "'. Perhaps the Cassandra user does not have the necessary permissions"); }); + return PathUtils.tryGetSpace(new File(dir).toPath(), getSpace, e -> { + throw new ConfigurationException("Unable check disk space in '" + dir + "'. Perhaps the Cassandra user does not have the necessary permissions"); + }); } public static IEndpointSnitch createEndpointSnitch(boolean dynamic, String snitchClassName) throws ConfigurationException @@ -1546,6 +1567,7 @@ public class DatabaseDescriptor { DatabaseDescriptor.cryptoProvider = cryptoProvider; } + public static IAuthenticator getAuthenticator() { return authenticator; @@ -1618,7 +1640,7 @@ public class DatabaseDescriptor { int defaultCidrGroupsCacheRefreshInterval = 5; // mins - if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null) + if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null) return defaultCidrGroupsCacheRefreshInterval; String cidrGroupsCacheRefreshInterval = conf.cidr_authorizer.parameters.get("cidr_groups_cache_refresh_interval"); @@ -1632,7 +1654,7 @@ public class DatabaseDescriptor { int defaultIpCacheMaxSize = 100; - if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null) + if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null) return defaultIpCacheMaxSize; String ipCacheMaxSize = conf.cidr_authorizer.parameters.get("ip_cache_max_size"); @@ -1675,8 +1697,8 @@ public class DatabaseDescriptor public static int getPermissionsUpdateInterval() { return conf.permissions_update_interval == null - ? conf.permissions_validity.toMilliseconds() - : conf.permissions_update_interval.toMilliseconds(); + ? conf.permissions_validity.toMilliseconds() + : conf.permissions_update_interval.toMilliseconds(); } public static void setPermissionsUpdateInterval(int updateInterval) @@ -1720,8 +1742,8 @@ public class DatabaseDescriptor public static int getRolesUpdateInterval() { return conf.roles_update_interval == null - ? conf.roles_validity.toMilliseconds() - : conf.roles_update_interval.toMilliseconds(); + ? conf.roles_validity.toMilliseconds() + : conf.roles_update_interval.toMilliseconds(); } public static void setRolesCacheActiveUpdate(boolean update) @@ -1852,7 +1874,7 @@ public class DatabaseDescriptor } catch (ConfigurationException e) { - throw new IllegalArgumentException("Bad configuration; unable to start server: "+e.getMessage()); + throw new IllegalArgumentException("Bad configuration; unable to start server: " + e.getMessage()); } catch (FSWriteError e) { @@ -1872,6 +1894,13 @@ public class DatabaseDescriptor /* For tests ONLY, don't use otherwise or all hell will break loose. Tests should restore value at the end. */ public static IPartitioner setPartitionerUnsafe(IPartitioner newPartitioner) + { + IPartitioner old = setOnlyPartitionerUnsafe(newPartitioner); + StorageService.instance.valueFactory = new VersionedValue.VersionedValueFactory(partitioner); + return old; + } + + public static IPartitioner setOnlyPartitionerUnsafe(IPartitioner newPartitioner) { IPartitioner old = partitioner; partitioner = newPartitioner; @@ -1882,6 +1911,7 @@ public class DatabaseDescriptor { return snitch; } + public static void setEndpointSnitch(IEndpointSnitch eps) { snitch = eps; @@ -1909,7 +1939,7 @@ public class DatabaseDescriptor public static void setColumnIndexSizeInKiB(int val) { - conf.column_index_size = val != -1 ? createIntKibibyteBoundAndEnsureItIsValidForByteConversion(val,"column_index_size") : null; + conf.column_index_size = val != -1 ? createIntKibibyteBoundAndEnsureItIsValidForByteConversion(val, "column_index_size") : null; } public static int getColumnIndexCacheSize() @@ -1924,7 +1954,7 @@ public class DatabaseDescriptor public static void setColumnIndexCacheSize(int val) { - conf.column_index_cache_size = createIntKibibyteBoundAndEnsureItIsValidForByteConversion(val,"column_index_cache_size"); + conf.column_index_cache_size = createIntKibibyteBoundAndEnsureItIsValidForByteConversion(val, "column_index_cache_size"); } public static int getBatchSizeWarnThreshold() @@ -1954,7 +1984,7 @@ public class DatabaseDescriptor public static void setBatchSizeWarnThresholdInKiB(int threshold) { - conf.batch_size_warn_threshold = createIntKibibyteBoundAndEnsureItIsValidForByteConversion(threshold,"batch_size_warn_threshold"); + conf.batch_size_warn_threshold = createIntKibibyteBoundAndEnsureItIsValidForByteConversion(threshold, "batch_size_warn_threshold"); } public static void setBatchSizeFailThresholdInKiB(int threshold) @@ -2021,7 +2051,8 @@ public class DatabaseDescriptor try { return UUID.fromString(REPLACE_NODE.getString()); - } catch (NullPointerException e) + } + catch (NullPointerException e) { return null; } @@ -2378,7 +2409,7 @@ public class DatabaseDescriptor public static void setStreamThroughputOutboundMebibytesPerSecAsInt(int value) { if (MEBIBYTES_PER_SECOND.toMegabitsPerSecond(value) >= Integer.MAX_VALUE) - throw new IllegalArgumentException("stream_throughput_outbound: " + value + + throw new IllegalArgumentException("stream_throughput_outbound: " + value + " is too large; it should be less than " + Integer.MAX_VALUE + " in megabits/s"); @@ -2502,10 +2533,10 @@ public class DatabaseDescriptor public static String[] getLocalSystemKeyspacesDataFileLocations() { if (useSpecificLocationForLocalSystemData()) - return new String[] {conf.local_system_data_file_directory}; + return new String[]{ conf.local_system_data_file_directory }; - return conf.data_file_directories.length == 0 ? conf.data_file_directories - : new String[] {conf.data_file_directories[0]}; + return conf.data_file_directories.length == 0 ? conf.data_file_directories + : new String[]{ conf.data_file_directories[0] }; } /** @@ -2562,11 +2593,11 @@ public class DatabaseDescriptor conf.flush_compression = compression; } - /** - * Maximum number of buffers in the compression pool. The default value is 3, it should not be set lower than that - * (one segment in compression, one written to, one in reserve); delays in compression may cause the log to use - * more, depending on how soon the sync policy stops all writing threads. - */ + /** + * Maximum number of buffers in the compression pool. The default value is 3, it should not be set lower than that + * (one segment in compression, one written to, one in reserve); delays in compression may cause the log to use + * more, depending on how soon the sync policy stops all writing threads. + */ public static int getCommitLogMaxCompressionBuffersInPool() { return conf.commitlog_max_compression_buffers_in_pool; @@ -2634,6 +2665,7 @@ public class DatabaseDescriptor * Update commitlog_segment_size in the tests. * {@link CommitLogSegmentManagerCDC} uses the CommitLogSegmentSize to estimate the file size on allocation. * It is important to keep the value unchanged for the estimation to be correct. + * * @param sizeMebibytes */ @VisibleForTesting /* Only for testing */ @@ -2729,7 +2761,7 @@ public class DatabaseDescriptor * refer to it as native address although some places still call it RPC address. It's not thrift RPC anymore * so native is more appropriate. The address alone is not enough to uniquely identify this instance because * multiple instances might use the same interface with different ports. - * + *

    * May be null, please use {@link FBUtilities#getBroadcastNativeAddressAndPort()} instead. */ public static InetAddress getBroadcastRpcAddress() @@ -2829,8 +2861,8 @@ public class DatabaseDescriptor } /** - * This is the port used with RPC address for the native protocol to communicate with clients. Now that thrift RPC - * is no longer in use there is no RPC port. + * This is the port used with RPC address for the native protocol to communicate with clients. Now that thrift RPC + * is no longer in use there is no RPC port. */ public static int getNativeTransportPort() { @@ -2872,7 +2904,7 @@ public class DatabaseDescriptor /** * If this value is set to <= 0 it will move auth requests to the standard request pool regardless of the current * size of the {@link org.apache.cassandra.transport.Dispatcher#authExecutor}'s active size. - * + *

    * see {@link org.apache.cassandra.transport.Dispatcher#dispatch} for executor selection */ public static void setNativeTransportMaxAuthThreads(int threads) @@ -3122,7 +3154,7 @@ public class DatabaseDescriptor { DurationSpec.IntMillisecondsBound blockMillis = conf.periodic_commitlog_sync_lag_block; return blockMillis == null - ? (long)(getCommitLogSyncPeriod() * 1.5) + ? (long) (getCommitLogSyncPeriod() * 1.5) : blockMillis.toMilliseconds(); } @@ -3211,6 +3243,7 @@ public class DatabaseDescriptor { conf.auto_snapshot = autoSnapshot; } + @VisibleForTesting public static boolean getAutoSnapshot() { @@ -3314,7 +3347,7 @@ public class DatabaseDescriptor public static File getSerializedCachePath(CacheType cacheType, String version, String extension) { String name = cacheType.toString() - + (version == null ? "" : '-' + version + '.' + extension); + + (version == null ? "" : '-' + version + '.' + extension); return new File(conf.saved_caches_directory, name); } @@ -3322,6 +3355,7 @@ public class DatabaseDescriptor { return conf.dynamic_snitch_update_interval.toMilliseconds(); } + public static void setDynamicUpdateInterval(int dynamicUpdateInterval) { conf.dynamic_snitch_update_interval = new DurationSpec.IntMillisecondsBound(dynamicUpdateInterval); @@ -3331,6 +3365,7 @@ public class DatabaseDescriptor { return conf.dynamic_snitch_reset_interval.toMilliseconds(); } + public static void setDynamicResetInterval(int dynamicResetInterval) { conf.dynamic_snitch_reset_interval = new DurationSpec.IntMillisecondsBound(dynamicResetInterval); @@ -3399,7 +3434,7 @@ public class DatabaseDescriptor public static long getMaxHintsFileSize() { - return conf.max_hints_file_size.toBytesInLong(); + return conf.max_hints_file_size.toBytesInLong(); } public static ParameterizedClass getHintsCompression() @@ -3513,7 +3548,9 @@ public class DatabaseDescriptor conf.key_cache_migrate_during_compaction = migrateCacheEntry; } - /** This method can return negative number for disabled */ + /** + * This method can return negative number for disabled + */ public static int getSSTablePreemptiveOpenIntervalInMiB() { if (conf.sstable_preemptive_open_interval == null) @@ -3521,7 +3558,9 @@ public class DatabaseDescriptor return conf.sstable_preemptive_open_interval.toMebibytes(); } - /** Negative number for disabled */ + /** + * Negative number for disabled + */ public static void setSSTablePreemptiveOpenIntervalInMiB(int mib) { if (mib < 0) @@ -3809,8 +3848,10 @@ public class DatabaseDescriptor { switch (datamodel) { - case "64": return true; - case "32": return false; + case "64": + return true; + case "32": + return false; } } String arch = OS_ARCH.getString(); @@ -4073,7 +4114,7 @@ public class DatabaseDescriptor public static FullQueryLoggerOptions getFullQueryLogOptions() { - return conf.full_query_logging_options; + return conf.full_query_logging_options; } public static void setFullQueryLogOptions(FullQueryLoggerOptions options) @@ -4572,7 +4613,10 @@ public class DatabaseDescriptor conf.row_index_read_size_fail_threshold = value; } - public static int getDefaultKeyspaceRF() { return conf.default_keyspace_rf; } + public static int getDefaultKeyspaceRF() + { + return conf.default_keyspace_rf; + } public static void setDefaultKeyspaceRF(int value) throws IllegalArgumentException { @@ -4665,11 +4709,13 @@ public class DatabaseDescriptor } } - public static DurationSpec.IntSecondsBound getStreamingSlowEventsLogTimeout() { + public static DurationSpec.IntSecondsBound getStreamingSlowEventsLogTimeout() + { return conf.streaming_slow_events_log_timeout; } - public static void setStreamingSlowEventsLogTimeout(String value) { + public static void setStreamingSlowEventsLogTimeout(String value) + { DurationSpec.IntSecondsBound next = new DurationSpec.IntSecondsBound(value); if (!conf.streaming_slow_events_log_timeout.equals(next)) { @@ -4785,6 +4831,7 @@ public class DatabaseDescriptor * both the more evolved cassandra.yaml approach but also the -XX param to override it on a one-off basis so you don't * have to change the full config of a node or a cluster in order to get a heap dump from a single node that's * misbehaving. + * * @return the absolute path of the -XX param if provided, else the heap_dump_path in cassandra.yaml */ public static Path getHeapDumpPath() @@ -4911,4 +4958,94 @@ public class DatabaseDescriptor { return conf == null ? new RepairRetrySpec() : conf.repair.retries; } + + public static int getCmsDefaultRetryMaxTries() + { + return conf.cms_default_max_retries; + } + + public static void setCmsDefaultRetryMaxTries(int value) + { + conf.cms_default_max_retries = value; + } + + public static DurationSpec getDefaultRetryBackoff() + { + return conf.cms_default_retry_backoff; + } + + public static DurationSpec getCmsAwaitTimeout() + { + return conf.cms_await_timeout; + } + + public static int getMetadataSnapshotFrequency() + { + return conf.metadata_snapshot_frequency; + } + + public static void setMetadataSnapshotFrequency(int frequency) + { + conf.metadata_snapshot_frequency = frequency; + } + + public static ConsistencyLevel getProgressBarrierMinConsistencyLevel() + { + return conf.progress_barrier_min_consistency_level; + } + + public static void setProgressBarrierMinConsistencyLevel(ConsistencyLevel newLevel) + { + conf.progress_barrier_min_consistency_level = newLevel; + } + + public static boolean getLogOutOfTokenRangeRequests() + { + return conf.log_out_of_token_range_requests; + } + + public static void setLogOutOfTokenRangeRequests(boolean enabled) + { + conf.log_out_of_token_range_requests = enabled; + } + + public static boolean getRejectOutOfTokenRangeRequests() + { + return conf.reject_out_of_token_range_requests; + } + + public static void setRejectOutOfTokenRangeRequests(boolean enabled) + { + conf.reject_out_of_token_range_requests = enabled; + } + + public static ConsistencyLevel getProgressBarrierDefaultConsistencyLevel() + { + return conf.progress_barrier_default_consistency_level; + } + + public static long getProgressBarrierTimeout(TimeUnit unit) + { + return conf.progress_barrier_timeout.to(unit); + } + + public static long getProgressBarrierBackoff(TimeUnit unit) + { + return conf.progress_barrier_backoff.to(unit); + } + + public static void setProgressBarrierTimeout(long timeOutInMillis) + { + conf.progress_barrier_timeout = new DurationSpec.LongMillisecondsBound(timeOutInMillis); + } + + public static void setProgressBarrierBackoff(long timeOutInMillis) + { + conf.progress_barrier_backoff = new DurationSpec.LongMillisecondsBound(timeOutInMillis); + } + + public static boolean getUnsafeTCMMode() + { + return conf.unsafe_tcm_mode; + } } diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java b/src/java/org/apache/cassandra/cql3/QueryProcessor.java index fba424d7d9..c879845266 100644 --- a/src/java/org/apache/cassandra/cql3/QueryProcessor.java +++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java @@ -18,28 +18,63 @@ package org.apache.cassandra.cql3; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; -import com.google.common.collect.*; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; import com.google.common.primitives.Ints; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.antlr.runtime.*; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.antlr.runtime.RecognitionException; import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.UDAggregate; +import org.apache.cassandra.cql3.functions.UDFunction; +import org.apache.cassandra.cql3.selection.ResultSetBuilder; +import org.apache.cassandra.cql3.statements.BatchStatement; +import org.apache.cassandra.cql3.statements.ModificationStatement; +import org.apache.cassandra.cql3.statements.QualifiedStatement; +import org.apache.cassandra.cql3.statements.SelectStatement; +import org.apache.cassandra.cql3.statements.schema.AlterSchemaStatement; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.db.ReadResponse; +import org.apache.cassandra.db.SinglePartitionReadQuery; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.PartitionIterators; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.CassandraException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.IsBootstrappingException; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.CQLMetrics; import org.apache.cassandra.metrics.ClientRequestMetrics; import org.apache.cassandra.metrics.ClientRequestsMetricsHolder; import org.apache.cassandra.net.Message; @@ -49,27 +84,20 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaChangeListener; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.cql3.functions.UDAggregate; -import org.apache.cassandra.cql3.functions.UDFunction; -import org.apache.cassandra.cql3.functions.Function; -import org.apache.cassandra.cql3.functions.FunctionName; -import org.apache.cassandra.cql3.selection.ResultSetBuilder; -import org.apache.cassandra.cql3.statements.*; -import org.apache.cassandra.db.*; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.RowIterator; -import org.apache.cassandra.db.partitions.PartitionIterator; -import org.apache.cassandra.db.partitions.PartitionIterators; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.exceptions.*; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.metrics.CQLMetrics; -import org.apache.cassandra.service.*; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.pager.QueryPager; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.transport.messages.ResultMessage; -import org.apache.cassandra.utils.*; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.MD5Digest; +import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.FutureCombiner; @@ -206,7 +234,6 @@ public class QueryProcessor implements QueryHandler private QueryProcessor() { - Schema.instance.registerListener(new StatementInvalidatingListener()); } @VisibleForTesting @@ -273,7 +300,7 @@ public class QueryProcessor implements QueryHandler private ResultMessage processNodeLocalWrite(CQLStatement statement, QueryState queryState, QueryOptions options) { - ClientRequestMetrics levelMetrics = ClientRequestsMetricsHolder.writeMetricsForLevel(ConsistencyLevel.NODE_LOCAL); + ClientRequestMetrics levelMetrics = ClientRequestsMetricsHolder.writeMetricsForLevel(ConsistencyLevel.NODE_LOCAL); ClientRequestMetrics globalMetrics = ClientRequestsMetricsHolder.writeMetrics; long startTime = nanoTime(); @@ -428,11 +455,16 @@ public class QueryProcessor implements QueryHandler qualifiedStatement.setKeyspace(clientState); keyspace = qualifiedStatement.keyspace(); } - // Note: if 2 threads prepare the same query, we'll live so don't bother synchronizing CQLStatement statement = raw.prepare(clientState); statement.validate(clientState); + // Set CQL string for AlterSchemaStatement as this is used to serialize the transformation + // in the cluster metadata log + if (statement instanceof AlterSchemaStatement) + ((AlterSchemaStatement)statement).setCql(query); + + if (isInternal) return new Prepared(statement, "", fullyQualified, keyspace); else @@ -641,7 +673,7 @@ public class QueryProcessor implements QueryHandler synchronized (this) { - CassandraVersion minVersion = Gossiper.instance.getMinVersion(DatabaseDescriptor.getWriteRpcTimeout(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + CassandraVersion minVersion = ClusterMetadata.current().directory.clusterMinVersion.cassandraVersion; if (minVersion != null && minVersion.compareTo(NEW_PREPARED_STATEMENT_BEHAVIOUR_SINCE_40, true) >= 0) { logger.info("Fully upgraded to at least {}", minVersion); @@ -854,7 +886,13 @@ public class QueryProcessor implements QueryHandler ((QualifiedStatement) statement).setKeyspace(clientState); Tracing.trace("Preparing statement"); - return statement.prepare(clientState); + CQLStatement prepared = statement.prepare(clientState); + // Set CQL string for AlterSchemaStatement as this is used to serialize the transformation + // in the cluster metadata log + if (prepared instanceof AlterSchemaStatement) + ((AlterSchemaStatement) prepared).setCql(queryStr); + + return prepared; } public static T parseStatement(String queryStr, Class klass, String type) throws SyntaxException @@ -917,6 +955,11 @@ public class QueryProcessor implements QueryHandler preparedStatements.asMap().clear(); } + public static void registerStatementInvalidatingListener() + { + Schema.instance.registerListener(new StatementInvalidatingListener()); + } + private static class StatementInvalidatingListener implements SchemaChangeListener { private static void removeInvalidPreparedStatements(String ksName, String cfName) @@ -1019,7 +1062,7 @@ public class QueryProcessor implements QueryHandler { // in case there are other overloads, we have to remove all overloads since argument type // matching may change (due to type casting) - if (Schema.instance.getKeyspaceMetadata(ksName).userFunctions.get(new FunctionName(ksName, functionName)).size() > 1) + if (!Schema.instance.getKeyspaceMetadata(ksName).userFunctions.get(new FunctionName(ksName, functionName)).isEmpty()) removeInvalidPreparedStatementsForFunction(ksName, functionName); } @@ -1077,4 +1120,4 @@ public class QueryProcessor implements QueryHandler removeInvalidPreparedStatementsForFunction(aggregate.name().keyspace, aggregate.name().name); } } -} +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java index 2b15c8d935..161ac8e353 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.cql3.functions; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; @@ -30,13 +31,20 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.schema.CQLTypeParser; import org.apache.cassandra.schema.Difference; import org.apache.cassandra.schema.UserFunctions; +import org.apache.cassandra.schema.Types; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.ByteBufferUtil; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; +import static org.apache.cassandra.db.TypeSizes.sizeof; import static org.apache.cassandra.utils.Clock.Global.nanoTime; /** @@ -44,6 +52,8 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime; */ public class UDAggregate extends UserFunction implements AggregateFunction { + public static final Serializer serializer = new Serializer(); + protected static final Logger logger = LoggerFactory.getLogger(UDAggregate.class); private final UDFDataType stateType; @@ -375,4 +385,69 @@ public class UDAggregate extends UserFunction implements AggregateFunction return builder.append(";") .toString(); } + + // Not quite a MetadataSerializer, or even a UDTAwareMetadataSerializer, as it needs the collection of UDFs during deserialization. + public static class Serializer + { + public void serialize(UDAggregate t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.name().keyspace); + out.writeUTF(t.name().name); + out.writeInt(t.argumentsList().size()); + for (String arg : t.argumentsList()) + out.writeUTF(arg); + out.writeUTF(t.returnType().asCQL3Type().toString()); + out.writeUTF(t.stateFunction.name().name); + out.writeUTF(t.stateType.toAbstractType().asCQL3Type().toString()); + out.writeBoolean(t.finalFunction() != null); + if (t.finalFunction() != null) + out.writeUTF(t.finalFunction().name().name); + out.writeBoolean(t.initialCondition() != null); + if (t.initialCondition() != null) + ByteBufferUtil.writeWithShortLength(t.initialCondition(), out); + } + + public UDAggregate deserialize(DataInputPlus in, Types types, Collection functions, Version version) throws IOException + { + String ks = in.readUTF(); + String name = in.readUTF(); + FunctionName fn = new FunctionName(ks, name); + int argCount = in.readInt(); + List> argList = new ArrayList<>(argCount); + for (int i = 0; i < argCount; i++) + argList.add(CQLTypeParser.parse(ks, in.readUTF(), types).udfType()); + AbstractType returnType = CQLTypeParser.parse(ks, in.readUTF(), types).udfType(); + FunctionName stateFunction = new FunctionName(ks, in.readUTF()); + AbstractType stateType = CQLTypeParser.parse(ks, in.readUTF(), types).udfType(); + boolean hasFinalFunction = in.readBoolean(); + FunctionName finalFunction = null; + if (hasFinalFunction) + finalFunction = new FunctionName(ks, in.readUTF()); + boolean hasInitialCondition = in.readBoolean(); + ByteBuffer initCond = null; + if (hasInitialCondition) + initCond = ByteBufferUtil.readWithShortLength(in); + return UDAggregate.create(functions, fn, argList, returnType, stateFunction, finalFunction, stateType, initCond); + } + + public long serializedSize(UDAggregate t, Version version) + { + long size = sizeof(t.name().keyspace) + sizeof(t.name().name); + + size += sizeof(t.argumentsList().size()); + for (String arg : t.argumentsList()) + size += sizeof(arg); + + size += sizeof(t.returnType().asCQL3Type().toString()); + size += sizeof(t.stateFunction.name().name); + size += sizeof(t.stateType.toAbstractType().asCQL3Type().toString()); + size += sizeof(t.finalFunction() != null); + if (t.finalFunction() != null) + size += sizeof(t.finalFunction().name().name); + size += sizeof(t.initialCondition() != null); + if (t.initialCondition() != null) + size += ByteBufferUtil.serializedSizeWithShortLength(t.initialCondition()); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java index 538d80e992..ce660acdef 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java @@ -17,11 +17,13 @@ */ package org.apache.cassandra.cql3.functions; +import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.net.InetAddress; import java.net.URL; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; @@ -34,6 +36,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import com.google.common.base.Objects; import com.google.common.collect.Lists; @@ -50,6 +53,10 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.FunctionExecutionException; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.schema.*; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.tracing.Tracing; @@ -59,6 +66,8 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; +import static org.apache.cassandra.db.TypeSizes.*; +import static org.apache.cassandra.schema.SchemaKeyspace.bbToString; import static org.apache.cassandra.utils.Clock.Global.nanoTime; /** @@ -66,6 +75,8 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime; */ public abstract class UDFunction extends UserFunction implements ScalarFunction { + public static final Serializer serializer = new Serializer(); + protected static final Logger logger = LoggerFactory.getLogger(UDFunction.class); static final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); @@ -732,4 +743,66 @@ public abstract class UDFunction extends UserFunction implements ScalarFunction return super.loadClass(name); } } + + public static class Serializer implements UDTAwareMetadataSerializer + { + public void serialize(UDFunction t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.name().keyspace); + out.writeUTF(t.name().name); + out.writeUTF(t.body()); + out.writeUTF(t.language()); + out.writeUTF(t.returnType().asCQL3Type().toString()); + out.writeBoolean(t.isCalledOnNullInput()); + List arguments = t.argNames().stream().map(c -> bbToString(c.bytes)).collect(Collectors.toList()); + out.writeInt(arguments.size()); + for (String argument : arguments) + out.writeUTF(argument); + + out.writeInt(t.argumentsList().size()); + for (String type : t.argumentsList()) + out.writeUTF(type); + } + + public UDFunction deserialize(DataInputPlus in, Types types, Version version) throws IOException + { + String keyspace = in.readUTF(); + String name = in.readUTF(); + FunctionName fn = new FunctionName(keyspace, name); + String body = in.readUTF(); + String language = in.readUTF(); + AbstractType returnType = CQLTypeParser.parse(keyspace, in.readUTF(), types).udfType(); + boolean isCalledOnNullInput = in.readBoolean(); + int argumentCount = in.readInt(); + List arguments = new ArrayList<>(argumentCount); + for (int i = 0; i < argumentCount; i++) + arguments.add(new ColumnIdentifier(in.readUTF(), true)); + + int argumentTypeCount = in.readInt(); + List> argTypes = new ArrayList<>(argumentTypeCount); + for (int i = 0; i < argumentTypeCount; i++) + argTypes.add(CQLTypeParser.parse(keyspace, in.readUTF(), types).udfType()); + + return UDFunction.create(fn, arguments, argTypes, returnType, isCalledOnNullInput, language, body); + } + + public long serializedSize(UDFunction t, Version version) + { + long size = sizeof(t.name().keyspace); + size += sizeof(t.name().name); + size += sizeof(t.body()); + size += sizeof(t.language()); + size += sizeof(t.returnType().asCQL3Type().toString()); + size += sizeof(t.isCalledOnNullInput()); + List arguments = t.argNames().stream().map(c -> bbToString(c.bytes)).collect(Collectors.toList()); + size += sizeof(arguments.size()); + for (String argument : arguments) + size += sizeof(argument); + + size += sizeof(t.argumentsList().size()); + for (String type : t.argumentsList()) + size += sizeof(type); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/cql3/functions/masking/ColumnMask.java b/src/java/org/apache/cassandra/cql3/functions/masking/ColumnMask.java index e8b7931718..de05aa58e2 100644 --- a/src/java/org/apache/cassandra/cql3/functions/masking/ColumnMask.java +++ b/src/java/org/apache/cassandra/cql3/functions/masking/ColumnMask.java @@ -18,6 +18,7 @@ package org.apache.cassandra.cql3.functions.masking; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -40,10 +41,19 @@ import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.FunctionName; import org.apache.cassandra.cql3.functions.FunctionResolver; import org.apache.cassandra.cql3.functions.ScalarFunction; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.ReversedType; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.CQLTypeParser; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.vint.VIntCoding; import static java.lang.String.format; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; @@ -67,6 +77,7 @@ import static org.apache.cassandra.cql3.statements.RequestValidations.invalidReq */ public class ColumnMask { + public static Serializer serializer = new Serializer(); public static final String DISABLED_ERROR_MESSAGE = "Cannot mask columns because dynamic data masking is not " + "enabled. You can enable it with the " + "dynamic_data_masking_enabled property on cassandra.yaml"; @@ -268,4 +279,72 @@ public class ColumnMask return format("%s(%s)", name, StringUtils.join(rawPartialArguments, ", ")); } } + + public static class Serializer + { + public void serialize(ColumnMask columnMask, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(columnMask.function.name().keyspace); + out.writeUTF(columnMask.function.name().name); + List> argTypes = columnMask.partialArgumentTypes(); + int numArgs = argTypes.size(); + out.writeUnsignedVInt32(numArgs); + for (int i = 0; i < numArgs; i++) + { + out.writeUTF(argTypes.get(i).asCQL3Type().toString()); + ByteBuffer value = columnMask.partialArgumentValues[i]; + out.writeBoolean(value != null); + if (value != null) + ByteBufferUtil.writeWithVIntLength(value, out); + } + } + + public ColumnMask deserialize(DataInputPlus in, String keyspace, AbstractType columnType, Types types, UserFunctions functions, Version version) throws IOException + { + FunctionName functionName = new FunctionName(in.readUTF(), in.readUTF()); + + int numArgs = in.readUnsignedVInt32(); + List> argTypes = new ArrayList<>(numArgs + 1); + argTypes.set(0, columnType); + ByteBuffer[] partialArgValues = new ByteBuffer[numArgs]; + for (int i = 0; i < numArgs; i++) + { + AbstractType argType = CQLTypeParser.parse(keyspace, in.readUTF(), types); + argTypes.set(i + 1, argType); + boolean valuePresent = in.readBoolean(); + partialArgValues[i] = valuePresent ? null : ByteBufferUtil.readWithVIntLength(in); + } + + Function function = FunctionResolver.get(keyspace, functionName, argTypes, null, null, null, functions); + if (function == null) + { + throw new AssertionError(format("Unable to find masking function %s(%s)", functionName, argTypes)); + } + else if (!(function instanceof ScalarFunction)) + { + throw new AssertionError(format("Function %s is not a scalar masking function", function)); + } + return new ColumnMask((ScalarFunction) function, partialArgValues); + } + + public long serializedSize(ColumnMask columnMask, Version version) + { + List> argTypes = columnMask.partialArgumentTypes(); + int numArgs = argTypes.size(); + long size = TypeSizes.sizeof(columnMask.function.name().keyspace) + + TypeSizes.sizeof(columnMask.function.name().name) + + VIntCoding.computeUnsignedVIntSize(numArgs); + + for (int i = 0; i < numArgs; i++) + { + size += TypeSizes.sizeof(argTypes.get(i).asCQL3Type().toString()); + size += TypeSizes.BOOL_SIZE; + ByteBuffer value = columnMask.partialArgumentValues[i]; + if (value != null) + size += ByteBufferUtil.serializedSizeWithVIntLength(value); + } + return size; + } + } + } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java index dc9324e03a..f7261e6768 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java @@ -177,8 +177,9 @@ public final class StatementRestrictions { this(type, table, allowFiltering); - final IndexRegistry indexRegistry = type.allowUseOfSecondaryIndices() ? IndexRegistry.obtain(table) : null; - + final IndexRegistry indexRegistry = type.allowUseOfSecondaryIndices() && allowUseOfSecondaryIndices + ? IndexRegistry.obtain(table) + : null; /* * WHERE clause. For a given entity, rules are: * - EQ relation conflicts with anything else (including a 2nd EQ) diff --git a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java index 90382dfe57..30e039e6b4 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DescribeStatement.java @@ -132,13 +132,9 @@ public abstract class DescribeStatement extends CQLStatement.Raw implements C @Override public ResultMessage executeLocally(QueryState state, QueryOptions options) { - DistributedSchema schema = Schema.instance.getDistributedSchemaBlocking(); - - Keyspaces keyspaces = Keyspaces.builder() - .add(schema.getKeyspaces()) - .add(Schema.instance.getLocalKeyspaces()) - .add(VirtualKeyspaceRegistry.instance.virtualKeyspacesMetadata()) - .build(); + Keyspaces keyspaces = Schema.instance.distributedAndLocalKeyspaces(); + UUID schemaVersion = Schema.instance.getVersion(); + keyspaces = keyspaces.with(VirtualKeyspaceRegistry.instance.virtualKeyspacesMetadata()); PagingState pagingState = options.getPagingState(); @@ -156,7 +152,7 @@ public abstract class DescribeStatement extends CQLStatement.Raw implements C // (vint bytes) serialized schema hash (currently the result of Keyspaces.hashCode()) // - long offset = getOffset(pagingState, schema.getVersion()); + long offset = getOffset(pagingState, schemaVersion); int pageSize = options.getPageSize(); Stream stream = describe(state.getClientState(), keyspaces); @@ -173,7 +169,7 @@ public abstract class DescribeStatement extends CQLStatement.Raw implements C ResultSet result = new ResultSet(resultMetadata, rows); if (pageSize > 0 && rows.size() == pageSize) - result.metadata.setHasMorePages(getPagingState(offset + pageSize, schema.getVersion())); + result.metadata.setHasMorePages(getPagingState(offset + pageSize, schemaVersion)); return new ResultMessage.Rows(result); } diff --git a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java index 65ec8fca67..a80e9ae693 100644 --- a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java +++ b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java @@ -82,7 +82,7 @@ public class PropertyDefinitions return properties.containsKey(name); } - protected String getString(String name) throws SyntaxException + public String getString(String name) throws SyntaxException { Object val = properties.get(name); if (val == null) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java index b8a27af3ba..14bdef0f68 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java @@ -18,21 +18,19 @@ package org.apache.cassandra.cql3.statements.schema; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLStatement; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.LocalStrategy; @@ -42,7 +40,11 @@ import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.utils.FBUtilities; @@ -52,9 +54,10 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_UNSA public final class AlterKeyspaceStatement extends AlterSchemaStatement { + private static final Logger logger = LoggerFactory.getLogger(AlterKeyspaceStatement.class); + private static final boolean allow_alter_rf_during_range_movement = ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT.getBoolean(); private static final boolean allow_unsafe_transient_changes = ALLOW_UNSAFE_TRANSIENT_CHANGES.getBoolean(); - private final HashSet clientWarnings = new HashSet<>(); private final KeyspaceAttributes attrs; private final boolean ifExists; @@ -66,10 +69,11 @@ public final class AlterKeyspaceStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { attrs.validate(); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) { @@ -83,17 +87,29 @@ public final class AlterKeyspaceStatement extends AlterSchemaStatement if (attrs.getReplicationStrategyClass() != null && attrs.getReplicationStrategyClass().equals(SimpleStrategy.class.getSimpleName())) Guardrails.simpleStrategyEnabled.ensureEnabled(state); + if (keyspace.params.replication.isMeta() && !keyspace.name.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) + throw ire("Can not alter a keyspace to use MetaReplicationStrategy"); + if (newKeyspace.params.replication.klass.equals(LocalStrategy.class)) throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); - newKeyspace.params.validate(keyspaceName, state); + newKeyspace.params.validate(keyspaceName, state, metadata); + newKeyspace.replicationStrategy.validate(metadata); validateNoRangeMovements(); - validateTransientReplication(keyspace.createReplicationStrategy(), newKeyspace.createReplicationStrategy()); + validateTransientReplication(keyspace, newKeyspace); - Keyspaces res = schema.withAddedOrUpdated(newKeyspace); + // Because we used to not properly validate unrecognized options, we only log a warning if we find one. + try + { + newKeyspace.replicationStrategy.validateExpectedOptions(metadata); + } + catch (ConfigurationException e) + { + logger.warn("Ignoring {}", e.getMessage()); + } - return res; + return schema.withAddedOrUpdated(newKeyspace); } SchemaChange schemaChangeEvent(KeyspacesDiff diff) @@ -109,13 +125,14 @@ public final class AlterKeyspaceStatement extends AlterSchemaStatement @Override Set clientWarnings(KeyspacesDiff diff) { + HashSet clientWarnings = new HashSet<>(); if (diff.isEmpty()) return clientWarnings; KeyspaceDiff keyspaceDiff = diff.altered.get(0); - AbstractReplicationStrategy before = keyspaceDiff.before.createReplicationStrategy(); - AbstractReplicationStrategy after = keyspaceDiff.after.createReplicationStrategy(); + AbstractReplicationStrategy before = keyspaceDiff.before.replicationStrategy; + AbstractReplicationStrategy after = keyspaceDiff.after.replicationStrategy; if (before.getReplicationFactor().fullReplicas < after.getReplicationFactor().fullReplicas) clientWarnings.add("When increasing replication factor you need to run a full (-full) repair to distribute the data."); @@ -128,29 +145,36 @@ public final class AlterKeyspaceStatement extends AlterSchemaStatement if (allow_alter_rf_during_range_movement) return; - Stream unreachableNotAdministrativelyInactive = - Gossiper.instance.getUnreachableMembers().stream().filter(endpoint -> !FBUtilities.getBroadcastAddressAndPort().equals(endpoint) && - !Gossiper.instance.isAdministrativelyInactiveState(endpoint)); - Stream endpoints = Stream.concat(Gossiper.instance.getLiveMembers().stream(), - unreachableNotAdministrativelyInactive); - List notNormalEndpoints = endpoints.filter(endpoint -> !FBUtilities.getBroadcastAddressAndPort().equals(endpoint) && - !Gossiper.instance.getEndpointStateForEndpoint(endpoint).isNormalState()) - .collect(Collectors.toList()); + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(FBUtilities.getBroadcastAddressAndPort()); + Set notNormalEndpoints = metadata.directory.states.entrySet().stream().filter(e -> !e.getKey().equals(nodeId)).filter(e -> { + switch (e.getValue()) + { + case BOOTSTRAPPING: + case LEAVING: + case MOVING: + return true; + default: + return false; + } + + }).map(e -> metadata.directory.endpoint(e.getKey())).collect(Collectors.toSet()); + if (!notNormalEndpoints.isEmpty()) { throw new ConfigurationException("Cannot alter RF while some endpoints are not in normal state (no range movements): " + notNormalEndpoints); } } - private void validateTransientReplication(AbstractReplicationStrategy oldStrategy, AbstractReplicationStrategy newStrategy) + private void validateTransientReplication(KeyspaceMetadata current, KeyspaceMetadata proposed) { //If there is no read traffic there are some extra alterations you can safely make, but this is so atypical //that a good default is to not allow unsafe changes if (allow_unsafe_transient_changes) return; - ReplicationFactor oldRF = oldStrategy.getReplicationFactor(); - ReplicationFactor newRF = newStrategy.getReplicationFactor(); + ReplicationFactor oldRF = current.replicationStrategy.getReplicationFactor(); + ReplicationFactor newRF = proposed.replicationStrategy.getReplicationFactor(); int oldTrans = oldRF.transientReplicas(); int oldFull = oldRF.fullReplicas; @@ -162,19 +186,13 @@ public final class AlterKeyspaceStatement extends AlterSchemaStatement if (DatabaseDescriptor.getNumTokens() > 1) throw new ConfigurationException(String.format("Transient replication is not supported with vnodes yet")); - Keyspace ks = Keyspace.open(keyspaceName); - for (ColumnFamilyStore cfs : ks.getColumnFamilyStores()) - { - if (cfs.viewManager.hasViews()) - { - throw new ConfigurationException("Cannot use transient replication on keyspaces using materialized views"); - } - if (cfs.indexManager.hasIndexes()) - { + if (!current.views.isEmpty()) + throw new ConfigurationException("Cannot use transient replication on keyspaces using materialized views"); + + for (TableMetadata table : current.tables) + if (!table.indexes.isEmpty()) throw new ConfigurationException("Cannot use transient replication on keyspaces using secondary indexes"); - } - } } //This is true right now because the transition from transient -> full lacks the pending state diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java index a539ea74e8..8907a93f8f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java @@ -34,6 +34,7 @@ import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.messages.ResultMessage; @@ -41,12 +42,46 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac { protected final String keyspaceName; // name of the keyspace affected by the statement protected ClientState state; + // TODO: not sure if this is going to stay the same, or will be replaced by more efficient serialization/sanitation means + // or just `toString` for every statement + private String cql; protected AlterSchemaStatement(String keyspaceName) { this.keyspaceName = keyspaceName; } + public void setCql(String cql) + { + this.cql = cql; + } + + @Override + public String cql() + { + assert cql != null; + return cql; + } + + @Override + public void enterExecution() + { + ClientWarn.instance.pauseCapture(); + ClientState localState = state; + if (localState != null) + localState.pauseGuardrails(); + } + + @Override + public void exitExecution() + { + ClientWarn.instance.resumeCapture(); + ClientState localState = state; + if (localState != null) + localState.resumeGuardrails(); + } + + // TODO: validation should be performed during application public void validate(ClientState state) { // validation is performed while executing the statement, in apply() @@ -57,7 +92,7 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) { - return execute(state, false); + return execute(state); } @Override @@ -68,7 +103,7 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac public ResultMessage executeLocally(QueryState state, QueryOptions options) { - return execute(state, true); + return execute(state); } /** @@ -100,7 +135,7 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac return ImmutableSet.of(); } - public ResultMessage execute(QueryState state, boolean locally) + public ResultMessage execute(QueryState state) { if (SchemaConstants.isLocalSystemKeyspace(keyspaceName)) throw ire("System keyspace '%s' is not user-modifiable", keyspaceName); @@ -110,12 +145,24 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac throw ire("Virtual keyspace '%s' is not user-modifiable", keyspaceName); validateKeyspaceName(); + // Perform a 'dry-run' attempt to apply the transformation locally before submitting to the CMS. This can save a + // round trip to the CMS for things syntax errors, but also fail fast for things like configuration errors. + // Such failures may be dependent on the specific node's config (for things like guardrails/memtable + // config/etc), but executing a schema change which has already been committed by the CMS should always succeed + // or else the node cannot make progress on any subsequent metadata changes. For this reason, validation errors + // during execution are trapped and the node will fall back to safe default config wherever possible. Attempting + // to apply the SchemaTransformation at this point will catch any such error which occurs locally before + // submission to the CMS, but it can't guarantee that the statement can be applied as-is on every node in the + // cluster, as config can be heterogenous falling back to safe defaults may occur on some nodes. + ClusterMetadata metadata = ClusterMetadata.current(); + apply(metadata); - SchemaTransformationResult result = Schema.instance.transform(this, locally); + ClusterMetadata result = Schema.instance.submit(this); - clientWarnings(result.diff).forEach(ClientWarn.instance::warn); + KeyspacesDiff diff = Keyspaces.diff(metadata.schema.getKeyspaces(), result.schema.getKeyspaces()); + clientWarnings(diff).forEach(ClientWarn.instance::warn); - if (result.diff.isEmpty()) + if (diff.isEmpty()) return new ResultMessage.Void(); /* @@ -127,9 +174,9 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac */ AuthenticatedUser user = state.getClientState().getUser(); if (null != user && !user.isAnonymous()) - createdResources(result.diff).forEach(r -> grantPermissionsOnResource(r, user)); + createdResources(diff).forEach(r -> grantPermissionsOnResource(r, user)); - return new ResultMessage.SchemaChange(schemaChangeEvent(result.diff)); + return new ResultMessage.SchemaChange(schemaChangeEvent(diff)); } private void validateKeyspaceName() @@ -170,4 +217,12 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac { return new InvalidRequestException(String.format(format, args)); } + + public String toString() + { + return "AlterSchemaStatement{" + + "keyspaceName='" + keyspaceName + '\'' + + ", cql='" + cql() + '\'' + + '}'; + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index fc0b4cffe6..24f9535d9f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -24,14 +24,15 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,38 +45,38 @@ import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.cql3.functions.masking.ColumnMask; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.AbstractType; - import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.gms.ApplicationState; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.index.TargetParser; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.MemtableParams; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.schema.Views; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.Directory; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.NoSpamLogger; - -import static java.lang.String.format; -import static java.lang.String.join; +import org.apache.cassandra.utils.Pair; import static com.google.common.collect.Iterables.isEmpty; -import static com.google.common.collect.Iterables.transform; - +import static java.lang.String.format; +import static java.lang.String.join; import static org.apache.cassandra.schema.TableMetadata.Flag; public abstract class AlterTableStatement extends AlterSchemaStatement @@ -100,8 +101,9 @@ public abstract class AlterTableStatement extends AlterSchemaStatement this.state = state; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); TableMetadata table = null == keyspace @@ -118,7 +120,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement if (table.isView()) throw ire("Cannot use ALTER TABLE on a materialized view; use ALTER MATERIALIZED VIEW instead"); - return schema.withAddedOrUpdated(apply(keyspace, table)); + return schema.withAddedOrUpdated(apply(metadata.nextEpoch(), keyspace, table)); } SchemaChange schemaChangeEvent(KeyspacesDiff diff) @@ -142,7 +144,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement return format("%s (%s, %s)", getClass().getSimpleName(), keyspaceName, tableName); } - abstract KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table); + abstract KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table); /** * {@code ALTER TABLE [IF EXISTS] ALTER TYPE ;} @@ -156,7 +158,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement super(keyspaceName, tableName, ifTableExists); } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { throw ire("Altering column types is no longer supported"); } @@ -196,7 +198,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement } @Override - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { ColumnMetadata column = table.getColumn(columnName); @@ -214,7 +216,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement if (Objects.equals(oldMask, newMask)) return keyspace; - TableMetadata.Builder tableBuilder = table.unbuild(); + TableMetadata.Builder tableBuilder = table.unbuild().epoch(epoch); tableBuilder.alterColumnMask(columnName, newMask); TableMetadata newTable = tableBuilder.build(); newTable.validate(); @@ -274,10 +276,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement newColumns.forEach(c -> c.type.validate(state, "Column " + c.name)); } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state); - TableMetadata.Builder tableBuilder = table.unbuild(); + TableMetadata.Builder tableBuilder = table.unbuild().epoch(epoch); Views.Builder viewsBuilder = keyspace.views.unbuild(); newColumns.forEach(c -> addColumn(keyspace, table, c, ifColumnNotExists, tableBuilder, viewsBuilder)); @@ -360,6 +362,38 @@ public abstract class AlterTableStatement extends AlterSchemaStatement } } + private static void validateIndexesForColumnModification(TableMetadata table, + ColumnIdentifier colId, + boolean isRename) + { + ColumnMetadata column = table.getColumn(colId); + Set dependentIndexes = new HashSet<>(); + for (IndexMetadata index : table.indexes) + { + Optional> target = TargetParser.tryParse(table, index); + if (target.isEmpty()) + { + // The target column(s) of this index is not trivially discernible from its metadata. + // This implies an external custom index implementation and without instantiating the + // index itself we cannot be sure that the column metadata is safe to modify. + dependentIndexes.add(index.name); + } + else if (target.get().left.equals(column)) + { + // The index metadata declares an explicit dependency on the column being modified, so + // the mutation must be rejected. + dependentIndexes.add(index.name); + } + } + if (!dependentIndexes.isEmpty()) + { + throw ire("Cannot %s column %s because it has dependent secondary indexes (%s)", + isRename ? "rename" : "drop", + colId, + join(", ", dependentIndexes)); + } + } + /** * {@code ALTER TABLE [IF EXISTS]
    DROP [IF EXISTS] } * {@code ALTER TABLE [IF EXISTS]
    DROP [IF EXISTS] ( , , ... )} @@ -379,7 +413,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement this.timestamp = timestamp; } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state); TableMetadata.Builder builder = table.unbuild(); @@ -407,14 +441,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement if (currentColumn.type.isUDT() && currentColumn.type.isMultiCell()) throw ire("Cannot drop non-frozen column %s of user type %s", column, currentColumn.type.asCQL3Type()); - // TODO: some day try and find a way to not rely on Keyspace/IndexManager/Index to find dependent indexes - Set dependentIndexes = Keyspace.openAndGetStore(table).indexManager.getDependentIndexes(currentColumn); - if (!dependentIndexes.isEmpty()) - { - throw ire("Cannot drop column %s because it has dependent secondary indexes (%s)", - currentColumn, - join(", ", transform(dependentIndexes, i -> i.name))); - } + if (!table.indexes.isEmpty()) + AlterTableStatement.validateIndexesForColumnModification(table, column, false); if (!isEmpty(keyspace.views.forTable(table.id))) throw ire("Cannot drop column %s on base table %s with materialized views", currentColumn, table.name); @@ -447,10 +475,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement this.ifColumnsExists = ifColumnsExists; } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state); - TableMetadata.Builder tableBuilder = table.unbuild(); + TableMetadata.Builder tableBuilder = table.unbuild().epoch(epoch); Views.Builder viewsBuilder = keyspace.views.unbuild(); renamedColumns.forEach((o, n) -> renameColumn(keyspace, table, o, n, ifColumnsExists, tableBuilder, viewsBuilder)); @@ -485,14 +513,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement table); } - // TODO: some day try and find a way to not rely on Keyspace/IndexManager/Index to find dependent indexes - Set dependentIndexes = Keyspace.openAndGetStore(table).indexManager.getDependentIndexes(column); - if (!dependentIndexes.isEmpty()) - { - throw ire("Can't rename column %s because it has dependent secondary indexes (%s)", - oldName, - join(", ", transform(dependentIndexes, i -> i.name))); - } + if (!table.indexes.isEmpty()) + AlterTableStatement.validateIndexesForColumnModification(table, oldName, true); for (ViewMetadata view : keyspace.views.forTable(table.id)) { @@ -523,13 +545,15 @@ public abstract class AlterTableStatement extends AlterSchemaStatement public void validate(ClientState state) { super.validate(state); - + // If a memtable configuration is specified, validate it against config + if (attrs.hasOption(TableParams.Option.MEMTABLE)) + MemtableParams.get(attrs.getString(TableParams.Option.MEMTABLE.toString())); Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state); validateDefaultTimeToLive(attrs.asNewTableParams()); } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { attrs.validate(); @@ -547,7 +571,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement "before being replayed."); } - if (keyspace.createReplicationStrategy().hasTransientReplicas() + if (keyspace.replicationStrategy.hasTransientReplicas() && params.readRepair != ReadRepairStrategy.NONE) { throw ire("read_repair must be set to 'NONE' for transiently replicated keyspaces"); @@ -573,7 +597,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement super(keyspaceName, tableName, ifTableExists); } - public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + public KeyspaceMetadata apply(Epoch epoch, KeyspaceMetadata keyspace, TableMetadata table) { if (!DatabaseDescriptor.enableDropCompactStorage()) throw new InvalidRequestException("DROP COMPACT STORAGE is disabled. Enable in cassandra.yaml to use."); @@ -587,7 +611,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement ? ImmutableSet.of(Flag.COMPOUND, Flag.COUNTER) : ImmutableSet.of(Flag.COMPOUND); - return keyspace.withSwapped(keyspace.tables.withSwapped(table.withSwapped(flags))); + return keyspace.withSwapped(keyspace.tables.withSwapped(table.unbuild().flags(flags).build())); } /** @@ -608,40 +632,45 @@ public abstract class AlterTableStatement extends AlterSchemaStatement Set preC15897nodes = new HashSet<>(); Set with2xSStables = new HashSet<>(); Splitter onComma = Splitter.on(',').omitEmptyStrings().trimResults(); - for (InetAddressAndPort node : StorageService.instance.getTokenMetadata().getAllEndpoints()) + Directory directory = ClusterMetadata.current().directory; + for (InetAddressAndPort node : directory.allAddresses()) { - if (MessagingService.instance().versions.knows(node) && - MessagingService.instance().versions.getRaw(node) < MessagingService.VERSION_40) + + CassandraVersion version = directory.version(directory.peerId(node)).cassandraVersion; + + if (version.compareTo(CassandraVersion.CASSANDRA_4_0) < 0) { + // if the cluster contains any pre-4.0 nodes (which really shouldn't be the case), reject this + // operation as we can't be certain all peers can support it. before4.add(node); - continue; } - - String sstableVersionsString = Gossiper.instance.getApplicationState(node, ApplicationState.SSTABLE_VERSIONS); - if (sstableVersionsString == null) + else { - preC15897nodes.add(node); - continue; - } - - try - { - boolean has2xSStables = onComma.splitToList(sstableVersionsString) - .stream() - .anyMatch(v -> v.compareTo("big-ma")<=0); - if (has2xSStables) - with2xSStables.add(node); - } - catch (IllegalArgumentException e) - { - // Means VersionType::fromString didn't parse a version correctly. Which shouldn't happen, we shouldn't - // have garbage in Gossip. But crashing the request is not ideal, so we log the error but ignore the - // node otherwise. - noSpamLogger.error("Unexpected error parsing sstable versions from gossip for {} (gossiped value " + - "is '{}'). This is a bug and should be reported. Cannot ensure that {} has no " + - "non-upgraded 2.x sstables anymore. If after this DROP COMPACT STORAGE some old " + - "sstables cannot be read anymore, please use `upgradesstables` with the " + - "`--force-compact-storage-on` option.", node, sstableVersionsString, node); + // any peer on a version greater than 4.0.0 must include CASSANDRA-15897, so just check that + // its min sstable version. Note: this app state may be empty/unset if the full StorageService + // initialisation hasn't been done, i.e. in tests. + String sstableVersionsString = Gossiper.instance.getApplicationState(node, ApplicationState.SSTABLE_VERSIONS); + if (Strings.isNullOrEmpty(sstableVersionsString)) + continue; + try + { + boolean has2xSStables = onComma.splitToList(sstableVersionsString) + .stream() + .anyMatch(v -> v.compareTo("big-ma")<=0); + if (has2xSStables) + with2xSStables.add(node); + } + catch (IllegalArgumentException e) + { + // Means VersionType::fromString didn't parse a version correctly. Which shouldn't happen, we shouldn't + // have garbage in Gossip. But crashing the request is not ideal, so we log the error but ignore the + // node otherwise. + noSpamLogger.error("Unexpected error parsing sstable versions from gossip for {} (gossiped value " + + "is '{}'). This is a bug and should be reported. Cannot ensure that {} has no " + + "non-upgraded 2.x sstables anymore. If after this DROP COMPACT STORAGE some old " + + "sstables cannot be read anymore, please use `upgradesstables` with the " + + "`--force-compact-storage-on` option.", node, sstableVersionsString, node); + } } } @@ -649,10 +678,6 @@ public abstract class AlterTableStatement extends AlterSchemaStatement throw new InvalidRequestException(format("Cannot DROP COMPACT STORAGE as some nodes in the cluster (%s) " + "are not on 4.0+ yet. Please upgrade those nodes and run " + "`upgradesstables` before retrying.", before4)); - if (!preC15897nodes.isEmpty()) - throw new InvalidRequestException(format("Cannot guarantee that DROP COMPACT STORAGE is safe as some nodes " + - "in the cluster (%s) do not have https://issues.apache.org/jira/browse/CASSANDRA-15897. " + - "Please upgrade those nodes and retry.", preC15897nodes)); if (!with2xSStables.isEmpty()) throw new InvalidRequestException(format("Cannot DROP COMPACT STORAGE as some nodes in the cluster (%s) " + "has some non-upgraded 2.x sstables. Please run `upgradesstables` " + diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java index 40bca4aac9..fefe70e1c6 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java @@ -34,6 +34,7 @@ import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -69,8 +70,10 @@ public abstract class AlterTypeStatement extends AlterSchemaStatement return new SchemaChange(Change.UPDATED, Target.TYPE, keyspaceName, typeName); } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); UserType type = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java index 7e707f476b..831e46b0ca 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -54,8 +55,10 @@ public final class AlterViewStatement extends AlterSchemaStatement this.state = state; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); ViewMetadata view = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java index eb9f33a949..8950f67d9a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java @@ -44,6 +44,7 @@ import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -89,7 +90,8 @@ public final class CreateAggregateStatement extends AlterSchemaStatement this.ifNotExists = ifNotExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { if (ifNotExists && orReplace) throw ire("Cannot use both 'OR REPLACE' and 'IF NOT EXISTS' directives"); @@ -105,6 +107,7 @@ public final class CreateAggregateStatement extends AlterSchemaStatement if (!rawStateType.isImplicitlyFrozen() && rawStateType.isFrozen()) throw ire("State type '%s' cannot be frozen; remove frozen<> modifier from '%s'", rawStateType, rawStateType); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java index f04ae37cd5..20923687ff 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java @@ -40,6 +40,7 @@ import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -82,7 +83,7 @@ public final class CreateFunctionStatement extends AlterSchemaStatement } // TODO: replace affected aggregates !! - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { if (ifNotExists && orReplace) throw ire("Cannot use both 'OR REPLACE' and 'IF NOT EXISTS' directives"); @@ -103,6 +104,7 @@ public final class CreateFunctionStatement extends AlterSchemaStatement if (!rawReturnType.isImplicitlyFrozen() && rawReturnType.isFrozen()) throw ire("Return type '%s' cannot be frozen; remove frozen<> modifier from '%s'", rawReturnType, rawReturnType); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java index b53e066f90..c1626d0f09 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java @@ -31,7 +31,6 @@ import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.cql3.statements.schema.IndexTarget.Type; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.MapType; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -40,6 +39,7 @@ import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -112,7 +112,8 @@ public final class CreateIndexStatement extends AlterSchemaStatement this.state = state; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { attrs.validate(); @@ -121,6 +122,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement if (attrs.isCustom && attrs.customClass.equals(SASIIndex.class.getName()) && !DatabaseDescriptor.getSASIIndexesEnabled()) throw new InvalidRequestException(SASI_INDEX_DISABLED); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire(KEYSPACE_DOES_NOT_EXIST, keyspaceName); @@ -143,7 +145,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement if (table.isView()) throw ire(MATERIALIZED_VIEWS_NOT_SUPPORTED); - if (Keyspace.open(table.keyspace).getReplicationStrategy().hasTransientReplicas()) + if (keyspace.replicationStrategy.hasTransientReplicas()) throw new InvalidRequestException(TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED); // guardrails to limit number of secondary indexes per table. diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java index 13d52b1e15..16a117c890 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java @@ -17,7 +17,6 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.HashSet; import java.util.Set; import com.google.common.collect.ImmutableSet; @@ -36,12 +35,11 @@ import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.locator.LocalStrategy; import org.apache.cassandra.locator.SimpleStrategy; -import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.KeyspaceParams.Option; -import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -51,7 +49,6 @@ public final class CreateKeyspaceStatement extends AlterSchemaStatement private final KeyspaceAttributes attrs; private final boolean ifNotExists; - private final HashSet clientWarnings = new HashSet<>(); public CreateKeyspaceStatement(String keyspaceName, KeyspaceAttributes attrs, boolean ifNotExists) { @@ -60,7 +57,7 @@ public final class CreateKeyspaceStatement extends AlterSchemaStatement this.ifNotExists = ifNotExists; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { attrs.validate(); @@ -70,6 +67,7 @@ public final class CreateKeyspaceStatement extends AlterSchemaStatement if (attrs.getReplicationStrategyClass() != null && attrs.getReplicationStrategyClass().equals(SimpleStrategy.class.getSimpleName())) Guardrails.simpleStrategyEnabled.ensureEnabled("SimpleStrategy", state); + Keyspaces schema = metadata.schema.getKeyspaces(); if (schema.containsKeyspace(keyspaceName)) { if (ifNotExists) @@ -83,7 +81,11 @@ public final class CreateKeyspaceStatement extends AlterSchemaStatement if (keyspace.params.replication.klass.equals(LocalStrategy.class)) throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); - keyspace.params.validate(keyspaceName, state); + if (keyspace.params.replication.isMeta()) + throw ire("Can not create a keyspace with MetaReplicationStrategy"); + + keyspace.params.validate(keyspaceName, state, metadata); + keyspace.replicationStrategy.validateExpectedOptions(metadata); return schema.withAddedOrUpdated(keyspace); } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java index 747837f305..b606a96d99 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -34,15 +34,16 @@ import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.functions.masking.ColumnMask; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -94,8 +95,9 @@ public final class CreateTableStatement extends AlterSchemaStatement this.useCompactStorage = useCompactStorage; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); @@ -108,10 +110,13 @@ public final class CreateTableStatement extends AlterSchemaStatement throw new AlreadyExistsException(keyspaceName, tableName); } - TableMetadata table = builder(keyspace.types).build(); + TableMetadata.Builder builder = builder(keyspace.types).epoch(metadata.nextEpoch()); + if (!builder.hasId() && !DatabaseDescriptor.useDeterministicTableID()) + builder.id(TableId.get(metadata)); + TableMetadata table = builder.build(); table.validate(); - if (keyspace.createReplicationStrategy().hasTransientReplicas() + if (keyspace.replicationStrategy.hasTransientReplicas() && table.params.readRepair != ReadRepairStrategy.NONE) { throw ire("read_repair must be set to 'NONE' for transiently replicated keyspaces"); @@ -128,6 +133,10 @@ public final class CreateTableStatement extends AlterSchemaStatement { super.validate(state); + // If a memtable configuration is specified, validate it against config + if (attrs.hasOption(TableParams.Option.MEMTABLE)) + MemtableParams.get(attrs.getString(TableParams.Option.MEMTABLE.toString())); + // Guardrail on table properties Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state); @@ -139,8 +148,7 @@ public final class CreateTableStatement extends AlterSchemaStatement { int totalUserTables = Schema.instance.getUserKeyspaces() .stream() - .map(Keyspace::open) - .mapToInt(keyspace -> keyspace.getColumnFamilyStores().size()) + .mapToInt(ksm -> ksm.tables.size()) .sum(); Guardrails.tables.guard(totalUserTables + 1, tableName, false, state); } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java index e85ffd80ae..392996b2f3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTriggerStatement.java @@ -24,6 +24,7 @@ import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.triggers.TriggerExecutor; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -45,8 +46,10 @@ public final class CreateTriggerStatement extends AlterSchemaStatement this.ifNotExists = ifNotExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java index d76c8089f6..bd63310f18 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTypeStatement.java @@ -34,6 +34,7 @@ import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Types; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -75,8 +76,9 @@ public final class CreateTypeStatement extends AlterSchemaStatement } } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java index 05629e00fd..a4a8ddcfbe 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java @@ -41,6 +41,7 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -110,7 +111,8 @@ public final class CreateViewStatement extends AlterSchemaStatement this.state = state; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { if (!DatabaseDescriptor.getMaterializedViewsEnabled()) throw ire("Materialized views are disabled. Enable in cassandra.yaml to use."); @@ -119,11 +121,12 @@ public final class CreateViewStatement extends AlterSchemaStatement * Basic dependency validations */ + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) throw ire("Keyspace '%s' doesn't exist", keyspaceName); - if (keyspace.createReplicationStrategy().hasTransientReplicas()) + if (keyspace.replicationStrategy.hasTransientReplicas()) throw new InvalidRequestException("Materialized views are not supported on transiently replicated keyspaces"); TableMetadata table = keyspace.tables.getNullable(tableName); @@ -324,6 +327,8 @@ public final class CreateViewStatement extends AlterSchemaStatement if (attrs.hasProperty(TableAttributes.ID)) builder.id(attrs.getId()); + else if (!builder.hasId() && !DatabaseDescriptor.useDeterministicTableID()) + builder.id(TableId.get(metadata)); builder.params(attrs.asNewTableParams()) .kind(TableMetadata.Kind.VIEW); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java index d83fbbf97f..e61332ee12 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropAggregateStatement.java @@ -35,6 +35,7 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -64,13 +65,14 @@ public final class DropAggregateStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { String name = argumentsSpeficied ? format("%s.%s(%s)", keyspaceName, aggregateName, join(", ", transform(arguments, CQL3Type.Raw::toString))) : format("%s.%s", keyspaceName, aggregateName); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) { diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java index af82206322..0353964683 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropFunctionStatement.java @@ -35,6 +35,7 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -65,13 +66,15 @@ public final class DropFunctionStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { String name = argumentsSpeficied ? format("%s.%s(%s)", keyspaceName, functionName, join(", ", transform(arguments, CQL3Type.Raw::toString))) : format("%s.%s", keyspaceName, functionName); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); if (null == keyspace) { diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java index 24b372d8c3..06cbc4be99 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropIndexStatement.java @@ -26,6 +26,7 @@ import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -42,8 +43,10 @@ public final class DropIndexStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); TableMetadata table = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java index 47e514a527..e074da54a3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropKeyspaceStatement.java @@ -25,6 +25,7 @@ import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; @@ -38,10 +39,12 @@ public final class DropKeyspaceStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { Guardrails.dropKeyspaceEnabled.ensureEnabled(state); + Keyspaces schema = metadata.schema.getKeyspaces(); if (schema.containsKeyspace(keyspaceName)) return schema.without(keyspaceName); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java index 78c98be3a7..56848a8c22 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTableStatement.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -47,10 +48,11 @@ public final class DropTableStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { Guardrails.dropTruncateTableEnabled.ensureEnabled(state); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); TableMetadata table = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java index 967e56834f..15007c3b1e 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTriggerStatement.java @@ -24,6 +24,7 @@ import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -42,8 +43,10 @@ public final class DropTriggerStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); TableMetadata table = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java index 97830c882a..105c8f5db8 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropTypeStatement.java @@ -31,6 +31,7 @@ import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; import org.apache.cassandra.transport.Event.SchemaChange; @@ -55,10 +56,12 @@ public final class DropTypeStatement extends AlterSchemaStatement } // TODO: expand types into tuples in all dropped columns of all tables - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { ByteBuffer name = bytes(typeName); + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); UserType type = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java index 2c73717546..121575503c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/DropViewStatement.java @@ -25,6 +25,7 @@ import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.schema.*; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; @@ -41,8 +42,10 @@ public final class DropViewStatement extends AlterSchemaStatement this.ifExists = ifExists; } - public Keyspaces apply(Keyspaces schema) + @Override + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); ViewMetadata view = null == keyspace diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index 93d477c847..87af6b840b 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -115,8 +115,8 @@ public final class TableAttributes extends PropertyDefinitions if (hasOption(COMPRESSION)) builder.compression(CompressionParams.fromMap(getMap(COMPRESSION))); - if (hasOption(MEMTABLE)) - builder.memtable(MemtableParams.get(getString(MEMTABLE))); + if (hasOption(Option.MEMTABLE)) + builder.memtable(MemtableParams.getWithFallback(getString(Option.MEMTABLE))); if (hasOption(DEFAULT_TIME_TO_LIVE)) builder.defaultTimeToLive(getInt(DEFAULT_TIME_TO_LIVE)); diff --git a/src/java/org/apache/cassandra/db/AbstractMutationVerbHandler.java b/src/java/org/apache/cassandra/db/AbstractMutationVerbHandler.java new file mode 100644 index 0000000000..cfea7eb45c --- /dev/null +++ b/src/java/org/apache/cassandra/db/AbstractMutationVerbHandler.java @@ -0,0 +1,185 @@ +/* + * 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.db; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.exceptions.CoordinatorBehindException; +import org.apache.cassandra.exceptions.InvalidRoutingException; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.utils.NoSpamLogger; + +public abstract class AbstractMutationVerbHandler implements IVerbHandler +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractMutationVerbHandler.class); + private static final String logMessageTemplate = "Received mutation from {} for token {} outside valid range for keyspace {}"; + + public void doVerb(Message message) throws IOException + { + processMessage(message, message.respondTo()); + } + + protected void processMessage(Message message, InetAddressAndPort respondTo) + { + if (message.epoch().isAfter(Epoch.EMPTY)) + { + ClusterMetadata metadata = ClusterMetadata.current(); + metadata = checkTokenOwnership(metadata, message); + metadata = checkSchemaVersion(metadata, message); + } + applyMutation(message, respondTo); + } + + abstract void applyMutation(Message message, InetAddressAndPort respondToAddress); + + private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message message) + { + String keyspace = message.payload.getKeyspaceName(); + DecoratedKey key = message.payload.key(); + + VersionedEndpoints.ForToken forToken = writePlacements(metadata, keyspace, key); + + if (message.epoch().isAfter(metadata.epoch)) + { + // If replica detects that coordinator has made an out-of-range request, it has to catch up blockingly, + // since coordinator's routing may be more recent. + if (!forToken.get().containsSelf()) + { + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + forToken = writePlacements(metadata, keyspace, key); + } + // Otherwise, coordinator and the replica agree about the placement of the givent token, so catch-up can be async + else + { + ClusterMetadataService.instance().fetchLogFromPeerOrCMSAsync(metadata, message.from(), message.epoch()); + } + } + + if (!forToken.get().containsSelf()) + { + StorageService.instance.incOutOfRangeOperationCount(); + Keyspace.open(message.payload.getKeyspaceName()).metric.outOfRangeTokenWrites.inc(); + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, message.from(), key.getToken(), message.payload.getKeyspaceName()); + throw InvalidRoutingException.forWrite(message.from(), key.getToken(), metadata.epoch, message.payload); + } + + if (forToken.lastModified().isAfter(message.epoch())) + { + TCMMetrics.instance.coordinatorBehindPlacements.mark(); + throw new CoordinatorBehindException(String.format("Routing is correct, but coordinator needs to catch-up at least to epoch %s to maintain consistency. Current coordinator epoch is %s", + forToken.lastModified(), message.epoch())); + } + + return metadata; + } + + private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message message) + { + if (SchemaConstants.isSystemKeyspace(message.payload.getKeyspaceName()) || message.epoch().is(metadata.epoch)) + return metadata; + String keyspace = message.payload.getKeyspaceName(); + Keyspace ks = metadata.schema.getKeyspace(keyspace); + if (ks != null) + { + if (message.epoch().isAfter(metadata.epoch)) + { + // coordinator is ahead - check each partition update if the schema is ahead of the schema we have for the table + for (PartitionUpdate pu : message.payload.getPartitionUpdates()) + { + Epoch remoteSchemaEpoch = pu.serializedAtEpoch; + if (remoteSchemaEpoch != null && remoteSchemaEpoch.isAfter(metadata.epoch)) + { + // the partition update was serialized after the epoch we currently know, catch up and + // make sure we've seen the epoch it has seen, otherwise fail request. + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + if (pu.serializedAtEpoch.isAfter(metadata.epoch)) + throw new IllegalStateException(String.format("Coordinator %s is still ahead after fetching log, our epoch = %s, their epoch = %s", + message.from(), + metadata.epoch, message.epoch())); + } + } + } + else if (message.epoch().isBefore(metadata.schema.lastModified())) + { + // coordinator might not have seen the latest schema change - check each modified table individually + for (PartitionUpdate pu : message.payload.getPartitionUpdates()) + { + // coordinator could be behind, check local tables + ColumnFamilyStore cfs = ks.getColumnFamilyStore(pu.metadata().id); + if (cfs != null) + { + Epoch remoteSchemaEpoch = pu.serializedAtEpoch; + if (remoteSchemaEpoch != null && remoteSchemaEpoch.isBefore(cfs.metadata().epoch)) + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(String.format("Coordinator %s is behind, our epoch = %s, their epoch = %s", + message.from(), + metadata.epoch, message.epoch())); + } + } + else + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(String.format("Schema mismatch, coordinator %s is behind, we're missing table %s.%s, our epoch = %s, their epoch = %s", + message.from(), + pu.metadata().keyspace, + pu.metadata().name, + metadata.epoch, message.epoch())); + } + } + } + } + else + { + if (message.epoch().isBefore(metadata.schema.lastModified())) + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(String.format("Schema mismatch, coordinator %s is behind, we're missing keyspace %s, our epoch = %s, their epoch = %s", + message.from(), + keyspace, + metadata.epoch, message.epoch())); + } + else + { + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + } + } + + return metadata; + } + + private static VersionedEndpoints.ForToken writePlacements(ClusterMetadata metadata, String keyspace, DecoratedKey key) + { + return metadata.placements.get(metadata.schema.getKeyspace(keyspace).getMetadata().params.replication).writes.forToken(key.getToken()); + } +} diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java index 1ef67878d5..83957a6260 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java @@ -90,14 +90,14 @@ import org.apache.cassandra.db.compaction.CompactionStrategyManager; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.memtable.Flushing; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.memtable.ShardBoundaries; import org.apache.cassandra.db.lifecycle.LifecycleNewTracker; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.lifecycle.SSTableSet; import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.db.lifecycle.View; -import org.apache.cassandra.db.memtable.Flushing; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.memtable.ShardBoundaries; import org.apache.cassandra.db.partitions.CachedPartition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.repair.CassandraTableRepairManager; @@ -161,6 +161,8 @@ import org.apache.cassandra.service.snapshot.SnapshotLoader; import org.apache.cassandra.service.snapshot.SnapshotManifest; import org.apache.cassandra.service.snapshot.TableSnapshot; import org.apache.cassandra.streaming.TableStreamManager; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.DefaultValue; import org.apache.cassandra.utils.ExecutorUtils; @@ -261,7 +263,8 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner static final String TOKEN_DELIMITER = ":"; /** Special values used when the local ranges are not changed with ring changes (e.g. local tables). */ - public static final int RING_VERSION_IRRELEVANT = -1; + // TODO - make this Epoch.EMPTY + public static final Epoch RING_VERSION_IRRELEVANT = Epoch.create(-1); static { @@ -375,26 +378,31 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner } public void reload() + { + reload(metadata()); + } + + public void reload(TableMetadata tableMetadata) { // metadata object has been mutated directly. make all the members jibe with new settings. // only update these runtime-modifiable settings if they have not been modified. if (!minCompactionThreshold.isModified()) for (ColumnFamilyStore cfs : concatWithIndexes()) - cfs.minCompactionThreshold = new DefaultValue(metadata().params.compaction.minCompactionThreshold()); + cfs.minCompactionThreshold = new DefaultValue<>(tableMetadata.params.compaction.minCompactionThreshold()); if (!maxCompactionThreshold.isModified()) for (ColumnFamilyStore cfs : concatWithIndexes()) - cfs.maxCompactionThreshold = new DefaultValue(metadata().params.compaction.maxCompactionThreshold()); + cfs.maxCompactionThreshold = new DefaultValue<>(tableMetadata.params.compaction.maxCompactionThreshold()); if (!crcCheckChance.isModified()) for (ColumnFamilyStore cfs : concatWithIndexes()) - cfs.crcCheckChance = new DefaultValue(metadata().params.crcCheckChance); + cfs.crcCheckChance = new DefaultValue<>(tableMetadata.params.crcCheckChance); - compactionStrategyManager.maybeReloadParamsFromSchema(metadata().params.compaction); + compactionStrategyManager.maybeReloadParamsFromSchema(tableMetadata.params.compaction); - indexManager.reload(); + indexManager.reload(tableMetadata); - memtableFactory = metadata().params.memtable.factory(); - switchMemtableOrNotify(FlushReason.SCHEMA_CHANGE, Memtable::metadataUpdated); + memtableFactory = tableMetadata.params.memtable.factory(); + switchMemtableOrNotify(FlushReason.SCHEMA_CHANGE, tableMetadata, Memtable::metadataUpdated); } public static Runnable getBackgroundCompactionTaskSubmitter() @@ -453,7 +461,9 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner { CompressionParams params = CompressionParams.fromMap(opts); params.validate(); - metadata.setLocalOverrides(metadata().unbuild().compression(params).build()); + + TableMetadata orig = metadata(); + metadata.setLocalOverrides(orig.unbuild().compression(params).epoch(orig.epoch).build()); } catch (ConfigurationException e) { @@ -470,27 +480,26 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner public ColumnFamilyStore(Keyspace keyspace, String columnFamilyName, Supplier sstableIdGenerator, - TableMetadataRef metadata, + TableMetadata initMetadata, Directories directories, boolean loadSSTables, - boolean registerBookeeping, - boolean offline) + boolean registerBookeeping) { assert directories != null; - assert metadata != null : "null metadata for " + keyspace + ':' + columnFamilyName; + assert initMetadata != null : "null metadata for " + keyspace + ':' + columnFamilyName; this.keyspace = keyspace; - this.metadata = metadata; + this.metadata = initMetadata.ref; this.directories = directories; name = columnFamilyName; - minCompactionThreshold = new DefaultValue<>(metadata.get().params.compaction.minCompactionThreshold()); - maxCompactionThreshold = new DefaultValue<>(metadata.get().params.compaction.maxCompactionThreshold()); - crcCheckChance = new DefaultValue<>(metadata.get().params.crcCheckChance); - viewManager = keyspace.viewManager.forTable(metadata.id); + minCompactionThreshold = new DefaultValue<>(initMetadata.params.compaction.minCompactionThreshold()); + maxCompactionThreshold = new DefaultValue<>(initMetadata.params.compaction.maxCompactionThreshold()); + crcCheckChance = new DefaultValue<>(initMetadata.params.crcCheckChance); + viewManager = keyspace.viewManager.forTable(initMetadata); this.sstableIdGenerator = sstableIdGenerator; sampleReadLatencyMicros = DatabaseDescriptor.getReadRpcTimeout(TimeUnit.MICROSECONDS) / 2; additionalWriteLatencyMicros = DatabaseDescriptor.getWriteRpcTimeout(TimeUnit.MICROSECONDS) / 2; - memtableFactory = metadata.get().params.memtable.factory(); + memtableFactory = initMetadata.params.memtable.factory(); logger.info("Initializing {}.{}", getKeyspaceName(), name); @@ -514,7 +523,7 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner { Directories.SSTableLister sstableFiles = directories.sstableLister(Directories.OnTxnErr.IGNORE).skipTemporary(true); sstables = SSTableReader.openAll(this, sstableFiles.list().entrySet(), metadata); - data.addInitialSSTablesWithoutUpdatingSize(sstables); + data.addInitialSSTablesWithoutUpdatingSize(sstables, this); } // compaction strategy should be created after the CFS has been prepared @@ -528,7 +537,7 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner // create the private ColumnFamilyStores for the secondary column indexes indexManager = new SecondaryIndexManager(this); - for (IndexMetadata info : metadata.get().indexes) + for (IndexMetadata info : initMetadata.indexes) { indexManager.addIndex(info, true); } @@ -563,7 +572,7 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner if (DatabaseDescriptor.isClientOrToolInitialized() || SchemaConstants.isSystemKeyspace(getKeyspaceName())) topPartitions = null; else - topPartitions = new TopPartitionTracker(metadata()); + topPartitions = new TopPartitionTracker(initMetadata); } public static String getTableMBeanName(String ks, String name, boolean isIndex) @@ -741,32 +750,31 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner } - public static ColumnFamilyStore createColumnFamilyStore(Keyspace keyspace, TableMetadataRef metadata, boolean loadSSTables) + public static ColumnFamilyStore createColumnFamilyStore(Keyspace keyspace, TableMetadata metadata, boolean loadSSTables) { return createColumnFamilyStore(keyspace, metadata.name, metadata, loadSSTables); } public static ColumnFamilyStore createColumnFamilyStore(Keyspace keyspace, - String columnFamily, - TableMetadataRef metadata, - boolean loadSSTables) + String columnFamily, + TableMetadata metadata, + boolean loadSSTables) { - Directories directories = new Directories(metadata.get()); - return createColumnFamilyStore(keyspace, columnFamily, metadata, directories, loadSSTables, true, false); + Directories directories = new Directories(metadata); + return createColumnFamilyStore(keyspace, columnFamily, metadata, directories, loadSSTables, true); } /** This is only directly used by offline tools */ public static synchronized ColumnFamilyStore createColumnFamilyStore(Keyspace keyspace, String columnFamily, - TableMetadataRef metadata, + TableMetadata metadata, Directories directories, boolean loadSSTables, - boolean registerBookkeeping, - boolean offline) + boolean registerBookkeeping) { return new ColumnFamilyStore(keyspace, columnFamily, directories.getUIDGenerator(SSTableIdFactory.instance.defaultBuilder()), - metadata, directories, loadSSTables, registerBookkeeping, offline); + metadata, directories, loadSSTables, registerBookkeeping); } /** @@ -963,10 +971,10 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner * Checks with the memtable if it should be switched for the given reason, and if not, calls the specified * notification method. */ - private void switchMemtableOrNotify(FlushReason reason, Consumer elseNotify) + private void switchMemtableOrNotify(FlushReason reason, TableMetadata metadata, Consumer elseNotify) { Memtable currentMemtable = data.getView().getCurrentMemtable(); - if (currentMemtable.shouldSwitch(reason)) + if (currentMemtable.shouldSwitch(reason, metadata)) switchMemtableIfCurrent(currentMemtable, reason); else elseNotify.accept(currentMemtable); @@ -1476,9 +1484,9 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner public static class VersionedLocalRanges extends ArrayList { - public final long ringVersion; + public final Epoch ringVersion; - public VersionedLocalRanges(long ringVersion, int initialSize) + public VersionedLocalRanges(Epoch ringVersion, int initialSize) { super(initialSize); this.ringVersion = ringVersion; @@ -1487,16 +1495,16 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner public VersionedLocalRanges localRangesWeighted() { + ClusterMetadata metadata = ClusterMetadata.current(); if (!SchemaConstants.isLocalSystemKeyspace(getKeyspaceName()) - && getPartitioner() == StorageService.instance.getTokenMetadata().partitioner) + && getPartitioner() == metadata.partitioner) { DiskBoundaryManager.VersionedRangesAtEndpoint versionedLocalRanges = DiskBoundaryManager.getVersionedLocalRanges(this); Set> localRanges = versionedLocalRanges.rangesAtEndpoint.ranges(); - long ringVersion = versionedLocalRanges.ringVersion; - + Epoch epoch = versionedLocalRanges.epoch; if (!localRanges.isEmpty()) { - VersionedLocalRanges weightedRanges = new VersionedLocalRanges(ringVersion, localRanges.size()); + VersionedLocalRanges weightedRanges = new VersionedLocalRanges(epoch, localRanges.size()); for (Range r : localRanges) { // WeightedRange supports only unwrapped ranges as it relies @@ -1509,7 +1517,7 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner } else { - return fullWeightedRange(ringVersion, getPartitioner()); + return fullWeightedRange(epoch, getPartitioner()); } } else @@ -1527,11 +1535,13 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner return ShardBoundaries.NONE; ShardBoundaries shardBoundaries = cachedShardBoundaries; + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + return ShardBoundaries.NONE; if (shardBoundaries == null || shardBoundaries.shardCount() != shardCount || - (shardBoundaries.ringVersion != RING_VERSION_IRRELEVANT && - shardBoundaries.ringVersion != StorageService.instance.getTokenMetadata().getRingVersion())) + (!shardBoundaries.epoch.equals(Epoch.EMPTY) && !shardBoundaries.epoch.equals(metadata.epoch))) { VersionedLocalRanges weightedRanges = localRangesWeighted(); @@ -1545,9 +1555,9 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner } @VisibleForTesting - public static VersionedLocalRanges fullWeightedRange(long ringVersion, IPartitioner partitioner) + public static VersionedLocalRanges fullWeightedRange(Epoch epoch, IPartitioner partitioner) { - VersionedLocalRanges ranges = new VersionedLocalRanges(ringVersion, 1); + VersionedLocalRanges ranges = new VersionedLocalRanges(epoch, 1); ranges.add(new Splitter.WeightedRange(1.0, new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken()))); return ranges; } @@ -3341,14 +3351,19 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner public DiskBoundaries getDiskBoundaries() { - return diskBoundaryManager.getDiskBoundaries(this); + return diskBoundaryManager.getDiskBoundaries(this, metadata.get()); + } + + public DiskBoundaries getDiskBoundaries(TableMetadata initialMetadata) + { + return diskBoundaryManager.getDiskBoundaries(this, initialMetadata); } public void invalidateLocalRanges() { diskBoundaryManager.invalidate(); - switchMemtableOrNotify(FlushReason.OWNED_RANGES_CHANGE, Memtable::localRangesUpdated); + switchMemtableOrNotify(FlushReason.OWNED_RANGES_CHANGE, metadata(), Memtable::localRangesUpdated); } @Override diff --git a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java index e4c7669806..2fef53a993 100644 --- a/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java +++ b/src/java/org/apache/cassandra/db/CounterMutationVerbHandler.java @@ -21,20 +21,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.StorageProxy; import static org.apache.cassandra.utils.Clock.Global.nanoTime; -public class CounterMutationVerbHandler implements IVerbHandler +public class CounterMutationVerbHandler extends AbstractMutationVerbHandler { public static final CounterMutationVerbHandler instance = new CounterMutationVerbHandler(); private static final Logger logger = LoggerFactory.getLogger(CounterMutationVerbHandler.class); - public void doVerb(final Message message) + protected void applyMutation(final Message message, InetAddressAndPort respondToAddress) { long queryStartNanoTime = nanoTime(); final CounterMutation cm = message.payload; @@ -50,7 +50,7 @@ public class CounterMutationVerbHandler implements IVerbHandler // it's own in that case. StorageProxy.applyCounterMutationOnLeader(cm, localDataCenter, - () -> MessagingService.instance().send(message.emptyResponse(), message.from()), + () -> MessagingService.instance().send(message.emptyResponse(), respondToAddress), queryStartNanoTime); } } diff --git a/src/java/org/apache/cassandra/db/DiskBoundaries.java b/src/java/org/apache/cassandra/db/DiskBoundaries.java index 7fe10f4c13..75868e35e6 100644 --- a/src/java/org/apache/cassandra/db/DiskBoundaries.java +++ b/src/java/org/apache/cassandra/db/DiskBoundaries.java @@ -27,28 +27,32 @@ import com.google.common.collect.ImmutableList; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; public class DiskBoundaries { public final List directories; public final ImmutableList positions; - final long ringVersion; + final Epoch epoch; final int directoriesVersion; private final ColumnFamilyStore cfs; private volatile boolean isInvalid = false; public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, int diskVersion) { - this(cfs, directories, null, -1, diskVersion); + this(cfs, directories, null, Epoch.EMPTY, diskVersion); } @VisibleForTesting - public DiskBoundaries(ColumnFamilyStore cfs, Directories.DataDirectory[] directories, List positions, long ringVersion, int diskVersion) + public DiskBoundaries(ColumnFamilyStore cfs, + Directories.DataDirectory[] directories, + List positions, + Epoch epoch, + int diskVersion) { this.directories = directories == null ? null : ImmutableList.copyOf(directories); this.positions = positions == null ? null : ImmutableList.copyOf(positions); - this.ringVersion = ringVersion; + this.epoch = epoch; this.directoriesVersion = diskVersion; this.cfs = cfs; } @@ -60,7 +64,7 @@ public class DiskBoundaries DiskBoundaries that = (DiskBoundaries) o; - if (ringVersion != that.ringVersion) return false; + if (!epoch.equals(that.epoch)) return false; if (directoriesVersion != that.directoriesVersion) return false; if (!directories.equals(that.directories)) return false; return positions != null ? positions.equals(that.positions) : that.positions == null; @@ -70,7 +74,7 @@ public class DiskBoundaries { int result = directories != null ? directories.hashCode() : 0; result = 31 * result + (positions != null ? positions.hashCode() : 0); - result = 31 * result + (int) (ringVersion ^ (ringVersion >>> 32)); + result = 31 * result + epoch.hashCode(); result = 31 * result + directoriesVersion; return result; } @@ -80,7 +84,7 @@ public class DiskBoundaries return "DiskBoundaries{" + "directories=" + directories + ", positions=" + positions + - ", ringVersion=" + ringVersion + + ", epoch=" + epoch + ", directoriesVersion=" + directoriesVersion + '}'; } @@ -93,8 +97,7 @@ public class DiskBoundaries if (isInvalid) return true; int currentDiskVersion = DisallowedDirectories.getDirectoriesVersion(); - long currentRingVersion = StorageService.instance.getTokenMetadata().getRingVersion(); - return currentDiskVersion != directoriesVersion || (ringVersion != -1 && currentRingVersion != ringVersion); + return currentDiskVersion != directoriesVersion; } public void invalidate() diff --git a/src/java/org/apache/cassandra/db/DiskBoundaryManager.java b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java index 7857d0cff8..13d1585858 100644 --- a/src/java/org/apache/cassandra/db/DiskBoundaryManager.java +++ b/src/java/org/apache/cassandra/db/DiskBoundaryManager.java @@ -31,9 +31,12 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.RangesAtEndpoint; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.PendingRangeCalculatorService; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; import org.apache.cassandra.utils.FBUtilities; public class DiskBoundaryManager @@ -43,18 +46,24 @@ public class DiskBoundaryManager public DiskBoundaries getDiskBoundaries(ColumnFamilyStore cfs) { - if (!cfs.getPartitioner().splitter().isPresent()) + return getDiskBoundaries(cfs, cfs.metadata()); + } + + public DiskBoundaries getDiskBoundaries(ColumnFamilyStore cfs, TableMetadata metadata) + { + if (!metadata.partitioner.splitter().isPresent()) return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), DisallowedDirectories.getDirectoriesVersion()); + if (diskBoundaries == null || diskBoundaries.isOutOfDate()) { synchronized (this) { if (diskBoundaries == null || diskBoundaries.isOutOfDate()) { - logger.debug("Refreshing disk boundary cache for {}.{}", cfs.getKeyspaceName(), cfs.getTableName()); + logger.trace("Refreshing disk boundary cache for {}.{}", cfs.getKeyspaceName(), cfs.getTableName()); DiskBoundaries oldBoundaries = diskBoundaries; - diskBoundaries = getDiskBoundaryValue(cfs); - logger.debug("Updating boundaries from {} to {} for {}.{}", oldBoundaries, diskBoundaries, cfs.getKeyspaceName(), cfs.getTableName()); + diskBoundaries = getDiskBoundaryValue(cfs, metadata.partitioner); + logger.trace("Updating boundaries from {} to {} for {}.{}", oldBoundaries, diskBoundaries, cfs.getKeyspaceName(), cfs.getTableName()); } } } @@ -70,12 +79,12 @@ public class DiskBoundaryManager static class VersionedRangesAtEndpoint { public final RangesAtEndpoint rangesAtEndpoint; - public final long ringVersion; + public final Epoch epoch; - VersionedRangesAtEndpoint(RangesAtEndpoint rangesAtEndpoint, long ringVersion) + VersionedRangesAtEndpoint(RangesAtEndpoint rangesAtEndpoint, Epoch epoch) { this.rangesAtEndpoint = rangesAtEndpoint; - this.ringVersion = ringVersion; + this.epoch = epoch; } } @@ -83,26 +92,35 @@ public class DiskBoundaryManager { RangesAtEndpoint localRanges; - long ringVersion; - TokenMetadata tmd; + Epoch epoch; + ClusterMetadata metadata; do { - tmd = StorageService.instance.getTokenMetadata(); - ringVersion = tmd.getRingVersion(); - localRanges = getLocalRanges(cfs, tmd); - logger.debug("Got local ranges {} (ringVersion = {})", localRanges, ringVersion); + metadata = ClusterMetadata.current(); + epoch = metadata.epoch; + localRanges = getLocalRanges(cfs, metadata); + logger.debug("Got local ranges {} (epoch = {})", localRanges, epoch); } - while (ringVersion != tmd.getRingVersion()); // if ringVersion is different here it means that - // it might have changed before we calculated localRanges - recalculate - - return new VersionedRangesAtEndpoint(localRanges, ringVersion); + while (!metadata.epoch.equals(ClusterMetadata.current().epoch)); // if epoch is different here it means that + // it might have changed before we calculated localRanges - recalculate + return new VersionedRangesAtEndpoint(localRanges, epoch); } - private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs) + private static DiskBoundaries getDiskBoundaryValue(ColumnFamilyStore cfs, IPartitioner partitioner) { - VersionedRangesAtEndpoint rangesAtEndpoint = getVersionedLocalRanges(cfs); - RangesAtEndpoint localRanges = rangesAtEndpoint.rangesAtEndpoint; - long ringVersion = rangesAtEndpoint.ringVersion; + if (ClusterMetadataService.instance() == null) + return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), null, Epoch.EMPTY, DisallowedDirectories.getDirectoriesVersion()); + + RangesAtEndpoint localRanges; + + ClusterMetadata metadata; + do + { + metadata = ClusterMetadata.current(); + localRanges = getLocalRanges(cfs, metadata); + logger.debug("Got local ranges {} (epoch = {})", localRanges, metadata.epoch); + } + while (metadata.epoch != ClusterMetadata.current().epoch); int directoriesVersion; Directories.DataDirectory[] dirs; @@ -114,29 +132,31 @@ public class DiskBoundaryManager while (directoriesVersion != DisallowedDirectories.getDirectoriesVersion()); // if directoriesVersion has changed we need to recalculate if (localRanges == null || localRanges.isEmpty()) - return new DiskBoundaries(cfs, dirs, null, ringVersion, directoriesVersion); + return new DiskBoundaries(cfs, dirs, null, metadata.epoch, directoriesVersion); - List positions = getDiskBoundaries(localRanges, cfs.getPartitioner(), dirs); + List positions = getDiskBoundaries(localRanges, partitioner, dirs); - return new DiskBoundaries(cfs, dirs, positions, ringVersion, directoriesVersion); + return new DiskBoundaries(cfs, dirs, positions, metadata.epoch, directoriesVersion); } - private static RangesAtEndpoint getLocalRanges(ColumnFamilyStore cfs, TokenMetadata tmd) + + private static RangesAtEndpoint getLocalRanges(ColumnFamilyStore cfs, ClusterMetadata metadata) { RangesAtEndpoint localRanges; + DataPlacement placement; if (StorageService.instance.isBootstrapMode() - && !StorageService.isReplacingSameAddress()) // When replacing same address, the node marks itself as UN locally + && !StorageService.isReplacingSameAddress()) // When replacing same address, the node marks itself as UN locally { - PendingRangeCalculatorService.instance.blockUntilFinished(); - localRanges = tmd.getPendingRanges(cfs.getKeyspaceName(), FBUtilities.getBroadcastAddressAndPort()); + placement = metadata.placements.get(cfs.keyspace.getMetadata().params.replication); } else { - // Reason we use use the future settled TMD is that if we decommission a node, we want to stream + // Reason we use use the future settled metadata is that if we decommission a node, we want to stream // from that node to the correct location on disk, if we didn't, we would put new files in the wrong places. // We do this to minimize the amount of data we need to move in rebalancedisks once everything settled - localRanges = cfs.keyspace.getReplicationStrategy().getAddressReplicas(tmd.cloneAfterAllSettled(), FBUtilities.getBroadcastAddressAndPort()); + placement = metadata.writePlacementAllSettled(cfs.keyspace.getMetadata()); } + localRanges = placement.writes.byEndpoint().get(FBUtilities.getBroadcastAddressAndPort()); return localRanges; } diff --git a/src/java/org/apache/cassandra/db/Keyspace.java b/src/java/org/apache/cassandra/db/Keyspace.java index a06c7e137a..ded130f8cc 100644 --- a/src/java/org/apache/cassandra/db/Keyspace.java +++ b/src/java/org/apache/cassandra/db/Keyspace.java @@ -56,13 +56,11 @@ import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.metrics.KeyspaceMetrics; import org.apache.cassandra.repair.KeyspaceRepairManager; import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.SchemaProvider; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.service.snapshot.TableSnapshot; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.ByteBufferUtil; @@ -91,6 +89,7 @@ public class Keyspace private static int TEST_FAIL_MV_LOCKS_COUNT = CassandraRelevantProperties.TEST_FAIL_MV_LOCKS_COUNT.getInt(); public final KeyspaceMetrics metric; + public final KeyspaceMetadataRef metadataRef; // It is possible to call Keyspace.open without a running daemon, so it makes sense to ensure // proper directories here as well as in CassandraDaemon. @@ -100,8 +99,6 @@ public class Keyspace DatabaseDescriptor.createAllDirectories(); } - private volatile KeyspaceMetadata metadata; - //OpOrder is defined globally since we need to order writes across //Keyspaces in the case of Views (batchlog of view mutations) public static final OpOrder writeOrder = new OpOrder(); @@ -109,12 +106,11 @@ public class Keyspace /* ColumnFamilyStore per column family */ private final ConcurrentMap columnFamilyStores = new ConcurrentHashMap<>(); - private volatile AbstractReplicationStrategy replicationStrategy; public final ViewManager viewManager; private final KeyspaceWriteHandler writeHandler; - private volatile ReplicationParams replicationParams; private final KeyspaceRepairManager repairManager; private final SchemaProvider schema; + private final String name; private static volatile boolean initialized = false; @@ -148,23 +144,15 @@ public class Keyspace public static Keyspace open(String keyspaceName) { assert initialized || SchemaConstants.isLocalSystemKeyspace(keyspaceName) : "Initialized: " + initialized; - return open(keyspaceName, Schema.instance, true); + Keyspace ks = Schema.instance.getKeyspaceInstance(keyspaceName); + assert ks != null : "Unknown keyspace " + keyspaceName; + return ks; } // to only be used by org.apache.cassandra.tools.Standalone* classes public static Keyspace openWithoutSSTables(String keyspaceName) { - return open(keyspaceName, Schema.instance, false); - } - - public static Keyspace open(String keyspaceName, SchemaProvider schema, boolean loadSSTables) - { - return schema.maybeAddKeyspaceInstance(keyspaceName, () -> new Keyspace(keyspaceName, schema, loadSSTables)); - } - - public static ColumnFamilyStore openAndGetStore(TableMetadataRef tableRef) - { - return open(tableRef.keyspace).getColumnFamilyStore(tableRef.id); + return Schema.instance.getKeyspaceInstance(keyspaceName); } public static ColumnFamilyStore openAndGetStore(TableMetadata table) @@ -188,15 +176,9 @@ public class Keyspace } } - public void setMetadata(KeyspaceMetadata metadata) - { - this.metadata = metadata; - createReplicationStrategy(metadata); - } - public KeyspaceMetadata getMetadata() { - return metadata; + return metadataRef.get(); } public Collection getColumnFamilyStores() @@ -216,7 +198,7 @@ public class Keyspace { ColumnFamilyStore cfs = columnFamilyStores.get(id); if (cfs == null) - throw new IllegalArgumentException("Unknown CF " + id); + throw new IllegalArgumentException(String.format("Unknown CF %s %s", id, columnFamilyStores)); return cfs; } @@ -317,38 +299,53 @@ public class Keyspace return getColumnFamilyStores().stream().flatMap(cfs -> cfs.listSnapshots().values().stream()); } + public static Keyspace forSchema(String keyspaceName, SchemaProvider schema) + { + return new Keyspace(keyspaceName, schema, true); + } + private Keyspace(String keyspaceName, SchemaProvider schema, boolean loadSSTables) + { + this(schema, schema.getKeyspaceMetadata(keyspaceName), loadSSTables); + } + + public Keyspace(SchemaProvider schema, KeyspaceMetadata metadata, boolean loadSSTables) { this.schema = schema; - metadata = schema.getKeyspaceMetadata(keyspaceName); - assert metadata != null : "Unknown keyspace " + keyspaceName; - + this.name = metadata.name; + + assert metadata != null : "Unknown keyspace " + metadata.name; + if (metadata.isVirtual()) - throw new IllegalStateException("Cannot initialize Keyspace with virtual metadata " + keyspaceName); - createReplicationStrategy(metadata); + throw new IllegalStateException("Cannot initialize Keyspace with virtual metadata " + metadata.name); this.metric = new KeyspaceMetrics(this); this.viewManager = new ViewManager(this); + + this.metadataRef = new KeyspaceMetadataRef(metadata, schema); for (TableMetadata cfm : metadata.tablesAndViews()) { logger.trace("Initializing {}.{}", getName(), cfm.name); - initCf(schema.getTableMetadataRef(cfm.id), loadSSTables); + initCf(cfm, loadSSTables); } - this.viewManager.reload(false); + + this.viewManager.reload(metadata); + this.metadataRef.unsetInitial(); this.repairManager = new CassandraKeyspaceRepairManager(this); this.writeHandler = new CassandraKeyspaceWriteHandler(this); } - private Keyspace(KeyspaceMetadata metadata) + public Keyspace(KeyspaceMetadata metadata) { this.schema = Schema.instance; - this.metadata = metadata; - createReplicationStrategy(metadata); + this.name = metadata.name; + this.metric = new KeyspaceMetrics(this); this.viewManager = new ViewManager(this); this.repairManager = new CassandraKeyspaceRepairManager(this); this.writeHandler = new CassandraKeyspaceWriteHandler(this); + this.metadataRef = new KeyspaceMetadataRef(metadata, schema); } public KeyspaceRepairManager getRepairManager() @@ -361,18 +358,6 @@ public class Keyspace return new Keyspace(metadata); } - private void createReplicationStrategy(KeyspaceMetadata ksm) - { - logger.info("Creating replication strategy " + ksm.name + " params " + ksm.params); - replicationStrategy = ksm.createReplicationStrategy(); - if (!ksm.params.replication.equals(replicationParams)) - { - logger.debug("New replication settings for keyspace {} - invalidating disk boundary caches", ksm.name); - columnFamilyStores.values().forEach(ColumnFamilyStore::invalidateLocalRanges); - } - replicationParams = ksm.params.replication; - } - // best invoked on the compaction manager. public void dropCf(TableId tableId, boolean dropData) { @@ -433,7 +418,7 @@ public class Keyspace /** * adds a cf to internal structures, ends up creating disk files). */ - public void initCf(TableMetadataRef metadata, boolean loadSSTables) + public void initCf(TableMetadata metadata, boolean loadSSTables) { ColumnFamilyStore cfs = columnFamilyStores.get(metadata.id); @@ -442,7 +427,8 @@ public class Keyspace // CFS being created for the first time, either on server startup or new CF being added. // We don't worry about races here; startup is safe, and adding multiple idential CFs // simultaneously is a "don't do that" scenario. - ColumnFamilyStore oldCfs = columnFamilyStores.putIfAbsent(metadata.id, ColumnFamilyStore.createColumnFamilyStore(this, metadata, loadSSTables)); + ColumnFamilyStore oldCfs = columnFamilyStores.putIfAbsent(metadata.id, + ColumnFamilyStore.createColumnFamilyStore(this, metadata, loadSSTables)); // CFS mbean instantiation will error out before we hit this, but in case that changes... if (oldCfs != null) throw new IllegalStateException("added multiple mappings for cf id " + metadata.id); @@ -452,7 +438,7 @@ public class Keyspace // re-initializing an existing CF. This will happen if you cleared the schema // on this node and it's getting repopulated from the rest of the cluster. assert cfs.name.equals(metadata.name); - cfs.reload(); + cfs.reload(metadata); } } @@ -514,7 +500,7 @@ public class Keyspace boolean isDeferrable, Promise future) { - if (TEST_FAIL_WRITES && metadata.name.equals(TEST_FAIL_WRITES_KS)) + if (TEST_FAIL_WRITES && getMetadata().name.equals(TEST_FAIL_WRITES_KS)) throw new RuntimeException("Testing write failures"); Lock[] locks = null; @@ -626,7 +612,7 @@ public class Keyspace try { Tracing.trace("Creating materialized view mutations from base table replica"); - viewManager.forTable(upd.metadata().id).pushViewReplicaUpdates(upd, makeDurable, baseComplete); + viewManager.forTable(upd.metadata()).pushViewReplicaUpdates(upd, makeDurable, baseComplete); } catch (Throwable t) { @@ -661,7 +647,7 @@ public class Keyspace public AbstractReplicationStrategy getReplicationStrategy() { - return replicationStrategy; + return getMetadata().replicationStrategy; } public List> flush(ColumnFamilyStore.FlushReason reason) @@ -749,6 +735,11 @@ public class Keyspace return Schema.instance.getKeyspaces().stream().map(Schema.instance::getKeyspaceInstance).filter(Objects::nonNull); } + public static Iterable nonSystem() + { + return Iterables.transform(Schema.instance.distributedKeyspaces().names(), Keyspace::open); + } + public static Iterable nonLocalStrategy() { return Iterables.transform(Schema.instance.distributedKeyspaces().names(), Keyspace::open); @@ -767,6 +758,37 @@ public class Keyspace public String getName() { - return metadata.name; + return name; + } + + private static class KeyspaceMetadataRef + { + // We need "initial" keyspace metadata for initCF to run, due to circular dependency + // between keyspace keyspace -> column family -> keyspace metadata. There are some + // calls within initCF that try accessing keyspace metadata, which requires the metadata + // of initializing keyspace to already be visible via ClusterMetadata#schema. + private KeyspaceMetadata initial; + + private final String name; + private final SchemaProvider provider; + + public KeyspaceMetadataRef(KeyspaceMetadata initial, SchemaProvider provider) + { + this.initial = initial; + this.name = initial.name; + this.provider = provider; + } + + public KeyspaceMetadata get() + { + if (initial != null) + return initial; + return provider.getKeyspaceMetadata(name); + } + + public void unsetInitial() + { + this.initial = null; + } } } diff --git a/src/java/org/apache/cassandra/db/MutationVerbHandler.java b/src/java/org/apache/cassandra/db/MutationVerbHandler.java index 1ab6711fdb..62b762f069 100644 --- a/src/java/org/apache/cassandra/db/MutationVerbHandler.java +++ b/src/java/org/apache/cassandra/db/MutationVerbHandler.java @@ -24,7 +24,7 @@ import org.apache.cassandra.tracing.Tracing; import static org.apache.cassandra.db.commitlog.CommitLogSegment.ENTRY_OVERHEAD_SIZE; -public class MutationVerbHandler implements IVerbHandler +public class MutationVerbHandler extends AbstractMutationVerbHandler { public static final MutationVerbHandler instance = new MutationVerbHandler(); @@ -51,7 +51,7 @@ public class MutationVerbHandler implements IVerbHandler InetAddressAndPort respondToAddress = message.respondTo(); try { - message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed()); + processMessage(message, respondToAddress); } catch (WriteTimeoutException wto) { @@ -59,6 +59,11 @@ public class MutationVerbHandler implements IVerbHandler } } + protected void applyMutation(Message message, InetAddressAndPort respondToAddress) + { + message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed()); + } + private static void forwardToLocalNodes(Message originalMessage, ForwardingInfo forwardTo) { Message.Builder builder = diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java index 7aa1e01b6a..4a08c23542 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java @@ -53,6 +53,7 @@ import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; /** @@ -65,25 +66,27 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR protected final DataRange dataRange; protected final Slices requestedSlices; - private PartitionRangeReadCommand(boolean isDigest, - int digestVersion, - boolean acceptsTransient, - TableMetadata metadata, - long nowInSec, - ColumnFilter columnFilter, - RowFilter rowFilter, - DataLimits limits, - DataRange dataRange, - Index.QueryPlan indexQueryPlan, - boolean trackWarnings) + @VisibleForTesting + protected PartitionRangeReadCommand(Epoch serializedAtEpoch, + boolean isDigest, + int digestVersion, + boolean acceptsTransient, + TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + DataRange dataRange, + Index.QueryPlan indexQueryPlan, + boolean trackWarnings) { - super(Kind.PARTITION_RANGE, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings); + super(serializedAtEpoch, Kind.PARTITION_RANGE, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings); this.dataRange = dataRange; this.requestedSlices = dataRange.clusteringIndexFilter.getSlices(metadata()); - } - private static PartitionRangeReadCommand create(boolean isDigest, + private static PartitionRangeReadCommand create(Epoch serializedAtEpoch, + boolean isDigest, int digestVersion, boolean acceptsTransient, TableMetadata metadata, @@ -109,7 +112,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR indexQueryPlan, trackWarnings); } - return new PartitionRangeReadCommand(isDigest, + return new PartitionRangeReadCommand(serializedAtEpoch, + isDigest, digestVersion, acceptsTransient, metadata, @@ -129,7 +133,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR DataLimits limits, DataRange dataRange) { - return create(false, + return create(metadata.epoch, + false, 0, false, metadata, @@ -152,7 +157,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR */ public static PartitionRangeReadCommand allDataRead(TableMetadata metadata, long nowInSec) { - return create(false, + return create(metadata.epoch, + false, 0, false, metadata, @@ -202,7 +208,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR // DataLimits.CQLGroupByLimits.GroupByAwareCounter assumes that if GroupingState.hasClustering(), then we're in // the middle of a group, but we can't make that assumption if we query and range "in advance" of where we are // on the ring. - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -217,7 +224,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR public PartitionRangeReadCommand copy() { - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -233,7 +241,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR @Override protected PartitionRangeReadCommand copyAsDigestQuery() { - return create(true, + return create(serializedAtEpoch(), + true, digestVersion(), false, metadata(), @@ -249,7 +258,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR @Override protected PartitionRangeReadCommand copyAsTransientQuery() { - return create(false, + return create(serializedAtEpoch(), + false, 0, true, metadata(), @@ -265,7 +275,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR @Override public PartitionRangeReadCommand withUpdatedLimit(DataLimits newLimits) { - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -281,7 +292,8 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR @Override public PartitionRangeReadCommand withUpdatedLimitsAndDataRange(DataLimits newLimits, DataRange newDataRange) { - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -515,6 +527,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR { public ReadCommand deserialize(DataInputPlus in, int version, + Epoch serializedAtEpoch, boolean isDigest, int digestVersion, boolean acceptsTransient, @@ -527,7 +540,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR throws IOException { DataRange range = DataRange.serializer.deserialize(in, version, metadata); - return PartitionRangeReadCommand.create(isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, range, indexQueryPlan, false); + return PartitionRangeReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, range, indexQueryPlan, false); } } @@ -545,7 +558,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR Index.QueryPlan indexQueryPlan, boolean trackWarnings) { - super(isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, dataRange, indexQueryPlan, trackWarnings); + super(metadata.epoch, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, dataRange, indexQueryPlan, trackWarnings); } @Override diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java index b8ac0a2229..84cc9bfa24 100644 --- a/src/java/org/apache/cassandra/db/ReadCommand.java +++ b/src/java/org/apache/cassandra/db/ReadCommand.java @@ -38,7 +38,10 @@ import org.slf4j.LoggerFactory; import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.config.*; import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.QueryCancelledException; +import org.apache.cassandra.exceptions.UnknownTableException; +import org.apache.cassandra.metrics.TCMMetrics; import org.apache.cassandra.net.MessageFlag; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.ParamType; @@ -67,9 +70,12 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.SchemaProvider; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.CassandraUInt; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.TimeUUID; @@ -100,6 +106,7 @@ public abstract class ReadCommand extends AbstractReadQuery private final boolean isDigestQuery; private final boolean acceptsTransient; + private final Epoch serializedAtEpoch; // if a digest query, the version for which the digest is expected. Ignored if not a digest. private int digestVersion; @@ -112,6 +119,7 @@ public abstract class ReadCommand extends AbstractReadQuery { public abstract ReadCommand deserialize(DataInputPlus in, int version, + Epoch serializedAtEpoch, boolean isDigest, int digestVersion, boolean acceptsTransient, @@ -136,7 +144,8 @@ public abstract class ReadCommand extends AbstractReadQuery } } - protected ReadCommand(Kind kind, + protected ReadCommand(Epoch serializedAtEpoch, + Kind kind, boolean isDigestQuery, int digestVersion, boolean acceptsTransient, @@ -158,6 +167,7 @@ public abstract class ReadCommand extends AbstractReadQuery this.acceptsTransient = acceptsTransient; this.indexQueryPlan = indexQueryPlan; this.trackWarnings = trackWarnings; + this.serializedAtEpoch = serializedAtEpoch; } public static ReadCommand getCommand() @@ -197,6 +207,15 @@ public abstract class ReadCommand extends AbstractReadQuery return isDigestQuery; } + /** + * the schema version on the table when serializing this read command + * @return + */ + public Epoch serializedAtEpoch() + { + return serializedAtEpoch; + } + /** * If the query is a digest one, the requested digest version. * @@ -1011,6 +1030,11 @@ public abstract class ReadCommand extends AbstractReadQuery @VisibleForTesting public static class Serializer implements IVersionedSerializer { + private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 10L, TimeUnit.SECONDS); + private static final NoSpamLogger.NoSpamLogStatement schemaMismatchStmt = + noSpamLogger.getStatement("Schema epoch mismatch during read command deserialization. " + + "TableId: {}, remote epoch: {}, local epoch: {}", 10L, TimeUnit.SECONDS); + private final SchemaProvider schema; public Serializer() @@ -1075,6 +1099,8 @@ public abstract class ReadCommand extends AbstractReadQuery if (command.isDigestQuery()) out.writeUnsignedVInt32(command.digestVersion()); command.metadata().id.serialize(out); + if (version >= MessagingService.VERSION_50) + Epoch.serializer.serialize(command.serializedAtEpoch, out); out.writeInt(version >= MessagingService.VERSION_50 ? CassandraUInt.fromLong(command.nowInSec()) : (int) command.nowInSec()); ColumnFilter.serializer.serialize(command.columnFilter(), out, version); RowFilter.serializer.serialize(command.rowFilter(), out, version); @@ -1098,28 +1124,47 @@ public abstract class ReadCommand extends AbstractReadQuery // better complain loudly than doing the wrong thing. if (isForThrift(flags)) throw new IllegalStateException("Received a command with the thrift flag set. " - + "This means thrift is in use in a mixed 3.0/3.X and 4.0+ cluster, " - + "which is unsupported. Make sure to stop using thrift before " - + "upgrading to 4.0"); + + "This means thrift is in use in a mixed 3.0/3.X and 4.0+ cluster, " + + "which is unsupported. Make sure to stop using thrift before " + + "upgrading to 4.0"); boolean hasIndex = hasIndex(flags); - int digestVersion = isDigest ? in.readUnsignedVInt32() : 0; - TableMetadata metadata = schema.getExistingTableMetadata(TableId.deserialize(in)); - long nowInSec = version >= MessagingService.VERSION_50 ? CassandraUInt.toLong(in.readInt()) : in.readInt(); - ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, metadata); - RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, metadata); - DataLimits limits = DataLimits.serializer.deserialize(in, version, metadata); + int digestVersion = isDigest ? (int)in.readUnsignedVInt() : 0; + TableId tableId = TableId.deserialize(in); + Epoch schemaVersion = null; + if (version >= MessagingService.VERSION_50) + schemaVersion = Epoch.serializer.deserialize(in); + TableMetadata tableMetadata; + try + { + tableMetadata = schema.getExistingTableMetadata(tableId); + } + catch (UnknownTableException e) + { + ClusterMetadata metadata = ClusterMetadata.current(); + Epoch localCurrentEpoch = metadata.epoch; + if (schemaVersion != null && localCurrentEpoch.isAfter(schemaVersion)) + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(e.getMessage()); + } + throw e; + } + long nowInSec = version >= MessagingService.VERSION_50 ? CassandraUInt.toLong(in.readInt()) : in.readInt(); + ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, tableMetadata); + RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, tableMetadata); + DataLimits limits = DataLimits.serializer.deserialize(in, version, tableMetadata); Index.QueryPlan indexQueryPlan = null; if (hasIndex) { - IndexMetadata index = deserializeIndexMetadata(in, version, metadata); - Index.Group indexGroup = Keyspace.openAndGetStore(metadata).indexManager.getIndexGroup(index); + IndexMetadata index = deserializeIndexMetadata(in, version, tableMetadata); + Index.Group indexGroup = Keyspace.openAndGetStore(tableMetadata).indexManager.getIndexGroup(index); if (indexGroup != null) indexQueryPlan = indexGroup.queryPlanFor(rowFilter); } - return kind.selectionDeserializer.deserialize(in, version, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan); + return kind.selectionDeserializer.deserialize(in, version, schemaVersion, isDigest, digestVersion, acceptsTransient, tableMetadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan); } private IndexMetadata deserializeIndexMetadata(DataInputPlus in, int version, TableMetadata metadata) throws IOException @@ -1144,6 +1189,7 @@ public abstract class ReadCommand extends AbstractReadQuery return 2 // kind + flags + (command.isDigestQuery() ? TypeSizes.sizeofUnsignedVInt(command.digestVersion()) : 0) + command.metadata().id.serializedSize() + + (version >= MessagingService.VERSION_50 ? Epoch.serializer.serializedSize(command.metadata().epoch) : 0) + TypeSizes.INT_SIZE // command.nowInSec() is serialized as uint + ColumnFilter.serializer.serializedSize(command.columnFilter(), version) + RowFilter.serializer.serializedSize(command.rowFilter(), version) diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java index d8069eebf4..5d430f32cf 100644 --- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java +++ b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java @@ -22,15 +22,24 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.exceptions.CoordinatorBehindException; +import org.apache.cassandra.exceptions.InvalidRoutingException; +import org.apache.cassandra.exceptions.QueryCancelledException; +import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.exceptions.QueryCancelledException; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.FBUtilities; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -42,16 +51,17 @@ public class ReadCommandVerbHandler implements IVerbHandler public void doVerb(Message message) { - if (StorageService.instance.isBootstrapMode()) + if (message.epoch().isAfter(Epoch.EMPTY)) { - throw new RuntimeException("Cannot service reads while bootstrapping!"); + ClusterMetadata metadata = ClusterMetadata.current(); + metadata = checkTokenOwnership(metadata, message); + metadata = checkSchemaVersion(metadata, message); } - ReadCommand command = message.payload; - validateTransientStatus(message); MessageParams.reset(); long timeout = message.expiresAtNanos() - message.createdAtNanos(); + ReadCommand command = message.payload; command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS)); if (message.trackWarnings()) @@ -103,40 +113,110 @@ public class ReadCommandVerbHandler implements IVerbHandler } } - private void validateTransientStatus(Message message) + private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message message) + { + ReadCommand readCommand = message.payload; + + if (SchemaConstants.isSystemKeyspace(readCommand.metadata().keyspace) || + readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes + return metadata; + + Keyspace ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace); + ColumnFamilyStore cfs = ks != null ? ks.getColumnFamilyStore(readCommand.metadata().id) : null; + Epoch localComparisonEpoch = metadata.epoch; + if (cfs != null) + localComparisonEpoch = cfs.metadata().epoch; + + if (localComparisonEpoch.isBefore(readCommand.serializedAtEpoch())) + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + else if (localComparisonEpoch.isAfter(readCommand.serializedAtEpoch())) + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(String.format("Coordinator schema for %s.%s with epoch %s is behind our schema %s", + message.payload.metadata().keyspace, + message.payload.metadata().name, + readCommand.serializedAtEpoch(), + localComparisonEpoch)); + } + ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace); + if (ks == null || ks.getColumnFamilyStore(readCommand.metadata().id) == null) + throw new IllegalStateException("Unknown table " + readCommand.metadata().id +" after fetching remote log entries"); + return metadata; + } + + private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message message) { ReadCommand command = message.payload; if (command.metadata().isVirtual()) - return; - Token token; + return metadata; + + if (command.isTopK()) + return metadata; if (command instanceof SinglePartitionReadCommand) - token = ((SinglePartitionReadCommand) command).partitionKey().getToken(); + { + Token token = ((SinglePartitionReadCommand) command).partitionKey().getToken(); + Replica localReplica = getLocalReplica(metadata, token, command.metadata().keyspace); + if (localReplica == null) + { + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + localReplica = getLocalReplica(metadata, token, command.metadata().keyspace); + } + if (localReplica == null) + { + StorageService.instance.incOutOfRangeOperationCount(); + Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc(); + throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, message.payload); + } + + if (!command.acceptsTransient() && localReplica.isTransient()) + { + MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS); + throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s", + command.acceptsTransient() ? "transient" : "full", + localReplica.isTransient() ? "transient" : "full", + this)); + } + } else - token = ((PartitionRangeReadCommand) command).dataRange().keyRange().right.getToken(); - - Replica replica = Keyspace.open(command.metadata().keyspace) - .getReplicationStrategy() - .getLocalReplicaFor(token); - - if (replica == null) { - if (command.isTopK()) - return; + AbstractBounds range = ((PartitionRangeReadCommand) command).dataRange().keyRange(); - logger.warn("Received a read request from {} for a range that is not owned by the current replica {}.", - message.from(), - command); - return; - } + // TODO: preexisting issue: for the range queries or queries that span multiple replicas, we can only make requests where the right token is owned, but not the left one + Replica maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace); + if (maxTokenLocalReplica == null) + { + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace); + } + if (maxTokenLocalReplica == null) + { + StorageService.instance.incOutOfRangeOperationCount(); + Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc(); + throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, message.payload); + } - if (!command.acceptsTransient() && replica.isTransient()) - { - MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS); - throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s", - command.acceptsTransient() ? "transient" : "full", - replica.isTransient() ? "transient" : "full", - this)); + + // TODO: preexisting issue: we should change the whole range for transient-ness, not just the right token + if (command.acceptsTransient() != maxTokenLocalReplica.isTransient()) + { + MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS); + throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s", + command.acceptsTransient() ? "transient" : "full", + maxTokenLocalReplica.isTransient() ? "transient" : "full", + this)); + } } + return metadata; + } + + private static Replica getLocalReplica(ClusterMetadata metadata, Token token, String keyspace) + { + return metadata.placements + .get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication) + .reads + .forToken(token) + .get() + .lookup(FBUtilities.getBroadcastAddressAndPort()); } } diff --git a/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java b/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java index 903b3d43bd..8ca29eba13 100644 --- a/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java +++ b/src/java/org/apache/cassandra/db/ReadRepairVerbHandler.java @@ -17,17 +17,17 @@ */ package org.apache.cassandra.db; -import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; -public class ReadRepairVerbHandler implements IVerbHandler +public class ReadRepairVerbHandler extends AbstractMutationVerbHandler { public static final ReadRepairVerbHandler instance = new ReadRepairVerbHandler(); - public void doVerb(Message message) + void applyMutation(Message message, InetAddressAndPort respondToAddress) { message.payload.apply(); - MessagingService.instance().send(message.emptyResponse(), message.from()); + MessagingService.instance().send(message.emptyResponse(), respondToAddress); } } diff --git a/src/java/org/apache/cassandra/db/ReadResponse.java b/src/java/org/apache/cassandra/db/ReadResponse.java index a9e2cec4a7..d4906b2dd5 100644 --- a/src/java/org/apache/cassandra/db/ReadResponse.java +++ b/src/java/org/apache/cassandra/db/ReadResponse.java @@ -34,6 +34,8 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; +import static org.apache.cassandra.db.RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO; + public abstract class ReadResponse { // Serializer for single partition read response @@ -48,6 +50,11 @@ public abstract class ReadResponse return new LocalDataResponse(data, command, rdi); } + public static ReadResponse createDataResponse(UnfilteredPartitionIterator data, ReadCommand command) + { + return new LocalDataResponse(data, command, NO_OP_REPAIRED_DATA_INFO); + } + public static ReadResponse createSimpleDataResponse(UnfilteredPartitionIterator data, ColumnFilter selection) { return new LocalDataResponse(data, selection); diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java index ae692fc5b8..aa7b72e4ef 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java @@ -76,6 +76,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.btree.BTreeSet; @@ -91,7 +92,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar protected final ClusteringIndexFilter clusteringIndexFilter; @VisibleForTesting - protected SinglePartitionReadCommand(boolean isDigest, + protected SinglePartitionReadCommand(Epoch serializedAtEpoch, + boolean isDigest, int digestVersion, boolean acceptsTransient, TableMetadata metadata, @@ -104,24 +106,25 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar Index.QueryPlan indexQueryPlan, boolean trackWarnings) { - super(Kind.SINGLE_PARTITION, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings); + super(serializedAtEpoch, Kind.SINGLE_PARTITION, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings); assert partitionKey.getPartitioner() == metadata.partitioner; this.partitionKey = partitionKey; this.clusteringIndexFilter = clusteringIndexFilter; } - private static SinglePartitionReadCommand create(boolean isDigest, - int digestVersion, - boolean acceptsTransient, - TableMetadata metadata, - long nowInSec, - ColumnFilter columnFilter, - RowFilter rowFilter, - DataLimits limits, - DecoratedKey partitionKey, - ClusteringIndexFilter clusteringIndexFilter, - Index.QueryPlan indexQueryPlan, - boolean trackWarnings) + private static SinglePartitionReadCommand create(Epoch serializedAtEpoch, + boolean isDigest, + int digestVersion, + boolean acceptsTransient, + TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + DecoratedKey partitionKey, + ClusteringIndexFilter clusteringIndexFilter, + Index.QueryPlan indexQueryPlan, + boolean trackWarnings) { if (metadata.isVirtual()) { @@ -138,7 +141,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar indexQueryPlan, trackWarnings); } - return new SinglePartitionReadCommand(isDigest, + return new SinglePartitionReadCommand(serializedAtEpoch, + isDigest, digestVersion, acceptsTransient, metadata, @@ -175,7 +179,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar ClusteringIndexFilter clusteringIndexFilter, Index.QueryPlan indexQueryPlan) { - return create(false, + return create(metadata.epoch, + false, 0, false, metadata, @@ -352,7 +357,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar public SinglePartitionReadCommand copy() { - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -369,7 +375,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar @Override protected SinglePartitionReadCommand copyAsDigestQuery() { - return create(true, + return create(serializedAtEpoch(), + true, digestVersion(), acceptsTransient(), metadata(), @@ -386,7 +393,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar @Override protected SinglePartitionReadCommand copyAsTransientQuery() { - return create(false, + return create(serializedAtEpoch(), + false, 0, true, metadata(), @@ -403,7 +411,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar @Override public SinglePartitionReadCommand withUpdatedLimit(DataLimits newLimits) { - return create(isDigestQuery(), + return create(serializedAtEpoch(), + isDigestQuery(), digestVersion(), acceptsTransient(), metadata(), @@ -1302,6 +1311,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar { public ReadCommand deserialize(DataInputPlus in, int version, + Epoch serializedAtEpoch, boolean isDigest, int digestVersion, boolean acceptsTransient, @@ -1315,7 +1325,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar { DecoratedKey key = metadata.partitioner.decorateKey(metadata.partitionKeyType.readBuffer(in, DatabaseDescriptor.getMaxValueSize())); ClusteringIndexFilter filter = ClusteringIndexFilter.serializer.deserialize(in, version, metadata); - return SinglePartitionReadCommand.create(isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, key, filter, indexQueryPlan, false); + return SinglePartitionReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, key, filter, indexQueryPlan, false); } } @@ -1362,7 +1372,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar Index.QueryPlan indexQueryPlan, boolean trackWarnings) { - super(isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, partitionKey, clusteringIndexFilter, indexQueryPlan, trackWarnings); + super(metadata.epoch, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, partitionKey, clusteringIndexFilter, indexQueryPlan, trackWarnings); } @Override diff --git a/src/java/org/apache/cassandra/db/SizeEstimatesRecorder.java b/src/java/org/apache/cassandra/db/SizeEstimatesRecorder.java index f9233bf904..56e8370cda 100644 --- a/src/java/org/apache/cassandra/db/SizeEstimatesRecorder.java +++ b/src/java/org/apache/cassandra/db/SizeEstimatesRecorder.java @@ -19,7 +19,10 @@ package org.apache.cassandra.db; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,15 +32,17 @@ import org.apache.cassandra.db.lifecycle.View; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaChangeListener; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Refs; +import static org.apache.cassandra.tcm.compatibility.TokenRingUtils.getAllRanges; import static org.apache.cassandra.utils.Clock.Global.nanoTime; /** @@ -62,8 +67,7 @@ public class SizeEstimatesRecorder implements SchemaChangeListener, Runnable public void run() { - TokenMetadata metadata = StorageService.instance.getTokenMetadata().cloneOnlyTokenMap(); - if (!metadata.isMember(FBUtilities.getBroadcastAddressAndPort())) + if (!ClusterMetadata.current().directory.allAddresses().contains(FBUtilities.getBroadcastAddressAndPort())) { logger.debug("Node is not part of the ring; not recording size estimates"); return; @@ -73,6 +77,9 @@ public class SizeEstimatesRecorder implements SchemaChangeListener, Runnable for (Keyspace keyspace : Keyspace.nonLocalStrategy()) { + if (keyspace.getMetadata().params.replication.isMeta()) + continue; + // In tools the call to describe_splits_ex() used to be coupled with the call to describe_local_ring() so // most access was for the local primary range; after creating the size_estimates table this was changed // to be the primary range. @@ -88,7 +95,7 @@ public class SizeEstimatesRecorder implements SchemaChangeListener, Runnable // range. If we publish multiple ranges downstream integrations may start to see duplicate data. // See CASSANDRA-15637 Collection> primaryRanges = StorageService.instance.getPrimaryRanges(keyspace.getName()); - Collection> localPrimaryRanges = StorageService.instance.getLocalPrimaryRange(); + Collection> localPrimaryRanges = getLocalPrimaryRange(); boolean rangesAreEqual = primaryRanges.equals(localPrimaryRanges); for (ColumnFamilyStore table : keyspace.getColumnFamilyStores()) { @@ -116,6 +123,34 @@ public class SizeEstimatesRecorder implements SchemaChangeListener, Runnable } } + @VisibleForTesting + public static Collection> getLocalPrimaryRange() + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId localNodeId = metadata.myNodeId(); + return getLocalPrimaryRange(metadata, localNodeId); + } + + @VisibleForTesting + public static Collection> getLocalPrimaryRange(ClusterMetadata metadata, NodeId nodeId) + { + String dc = metadata.directory.location(nodeId).datacenter; + Set tokens = new HashSet<>(metadata.tokenMap.tokens(nodeId)); + + // filter tokens to the single DC + List filteredTokens = Lists.newArrayList(); + for (Token token : metadata.tokenMap.tokens()) + { + NodeId owner = metadata.tokenMap.owner(token); + if (dc.equals(metadata.directory.location(owner).datacenter)) + filteredTokens.add(token); + } + return getAllRanges(filteredTokens).stream() + .filter(t -> tokens.contains(t.right)) + .collect(Collectors.toList()); + } + + @SuppressWarnings("resource") private static Map, Pair> computeSizeEstimates(ColumnFamilyStore table, Collection> ranges) { // for each local primary range, estimate (crudely) mean partition size and partitions count. diff --git a/src/java/org/apache/cassandra/db/SystemKeyspace.java b/src/java/org/apache/cassandra/db/SystemKeyspace.java index b8ecc5afb4..d751f6ea2c 100644 --- a/src/java/org/apache/cassandra/db/SystemKeyspace.java +++ b/src/java/org/apache/cassandra/db/SystemKeyspace.java @@ -36,7 +36,6 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.management.openmbean.OpenDataException; @@ -59,7 +58,6 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; -import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.compaction.CompactionHistoryTabularData; import org.apache.cassandra.db.marshal.ByteBufferAccessor; @@ -77,6 +75,9 @@ import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.HeartBeatState; +import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.io.sstable.SSTableId; import org.apache.cassandra.io.sstable.SequenceBasedSSTableId; import org.apache.cassandra.io.util.DataInputBuffer; @@ -110,6 +111,10 @@ import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.uncommitted.PaxosRows; import org.apache.cassandra.service.paxos.uncommitted.PaxosUncommittedIndex; import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.CassandraVersion; @@ -127,6 +132,14 @@ import static org.apache.cassandra.config.DatabaseDescriptor.paxosStatePurging; import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; import static org.apache.cassandra.cql3.QueryProcessor.executeInternalWithNowInSec; import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal; +import static org.apache.cassandra.gms.ApplicationState.DC; +import static org.apache.cassandra.gms.ApplicationState.HOST_ID; +import static org.apache.cassandra.gms.ApplicationState.INTERNAL_ADDRESS_AND_PORT; +import static org.apache.cassandra.gms.ApplicationState.NATIVE_ADDRESS_AND_PORT; +import static org.apache.cassandra.gms.ApplicationState.RACK; +import static org.apache.cassandra.gms.ApplicationState.RELEASE_VERSION; +import static org.apache.cassandra.gms.ApplicationState.STATUS_WITH_PORT; +import static org.apache.cassandra.gms.ApplicationState.TOKENS; import static org.apache.cassandra.service.paxos.Commit.latest; import static org.apache.cassandra.utils.CassandraVersion.NULL_VERSION; import static org.apache.cassandra.utils.CassandraVersion.UNREADABLE_VERSION; @@ -163,6 +176,10 @@ public final class SystemKeyspace public static final String PREPARED_STATEMENTS = "prepared_statements"; public static final String REPAIRS = "repairs"; public static final String TOP_PARTITIONS = "top_partitions"; + public static final String METADATA_LOG = "local_metadata_log"; + public static final String SNAPSHOT_TABLE_NAME = "metadata_snapshots"; + public static final String SEALED_PERIODS_TABLE_NAME = "metadata_sealed_periods"; + public static final String LAST_SEALED_PERIOD_TABLE_NAME = "metadata_last_sealed_period"; /** * By default the system keyspace tables should be stored in a single data directory to allow the server @@ -195,13 +212,15 @@ public final class SystemKeyspace COMPACTION_HISTORY, SSTABLE_ACTIVITY_V2, TABLE_ESTIMATES, TABLE_ESTIMATES_TYPE_PRIMARY, TABLE_ESTIMATES_TYPE_LOCAL_PRIMARY, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS, BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS, - LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY); + LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY, + METADATA_LOG, SNAPSHOT_TABLE_NAME, SEALED_PERIODS_TABLE_NAME, LAST_SEALED_PERIOD_TABLE_NAME); public static final Set TABLE_NAMES = ImmutableSet.of( BATCHES, PAXOS, PAXOS_REPAIR_HISTORY, BUILT_INDEXES, LOCAL, PEERS_V2, PEER_EVENTS_V2, COMPACTION_HISTORY, SSTABLE_ACTIVITY_V2, TABLE_ESTIMATES, AVAILABLE_RANGES_V2, TRANSFERRED_RANGES_V2, VIEW_BUILDS_IN_PROGRESS, BUILT_VIEWS, PREPARED_STATEMENTS, REPAIRS, TOP_PARTITIONS, LEGACY_PEERS, LEGACY_PEER_EVENTS, - LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY); + LEGACY_TRANSFERRED_RANGES, LEGACY_AVAILABLE_RANGES, LEGACY_SIZE_ESTIMATES, LEGACY_SSTABLE_ACTIVITY, + METADATA_LOG, SNAPSHOT_TABLE_NAME, SEALED_PERIODS_TABLE_NAME, LAST_SEALED_PERIOD_TABLE_NAME); public static final TableMetadata Batches = parse(BATCHES, @@ -466,7 +485,45 @@ public final class SystemKeyspace + "cfids set, " + "PRIMARY KEY (parent_id))").build(); - /** @deprecated See CASSANDRA-7544 */ + public static final TableMetadata LocalMetadataLog = + parse(METADATA_LOG, + "Local Metadata Log", + "CREATE TABLE %s (" + + "period bigint," + + "current_epoch bigint static," + + "epoch bigint," + + "entry_id bigint," + + "transformation blob," + + "kind text," + + "PRIMARY KEY (period, epoch))") + .compaction(CompactionParams.twcs(ImmutableMap.of("compaction_window_unit","DAYS", + "compaction_window_size","1"))) + .build(); + + public static final TableMetadata Snapshots = parse(SNAPSHOT_TABLE_NAME, + "ClusterMetadata snapshots", + "CREATE TABLE IF NOT EXISTS %s (" + + "epoch bigint PRIMARY KEY," + + "period bigint," + + "snapshot blob)") + .build(); + + public static final TableMetadata SealedPeriods = parse(SEALED_PERIODS_TABLE_NAME, + "ClusterMetadata sealed periods", + "CREATE TABLE IF NOT EXISTS %s (" + + "max_epoch bigint PRIMARY KEY," + + "period bigint)") + .partitioner(new LocalPartitioner(LongType.instance)) + .build(); + + public static final TableMetadata LastSealedPeriod = parse(LAST_SEALED_PERIOD_TABLE_NAME, + "ClusterMetadata last sealed period", + "CREATE TABLE IF NOT EXISTS %s (" + + "key text PRIMARY KEY," + + "epoch bigint," + + "period bigint)") + .build(); + @Deprecated(since = "4.0") private static final TableMetadata LegacyPeers = parse(LEGACY_PEERS, @@ -557,7 +614,11 @@ public final class SystemKeyspace BuiltViews, PreparedStatements, Repairs, - TopPartitions); + TopPartitions, + LocalMetadataLog, + LastSealedPeriod, + SealedPeriods, + Snapshots); } private static volatile Map> truncationRecords; @@ -567,16 +628,31 @@ public final class SystemKeyspace NEEDS_BOOTSTRAP, COMPLETED, IN_PROGRESS, - DECOMMISSIONED + DECOMMISSIONED; + + public static BootstrapState fromNodeState(NodeState nodeState) + { + if (nodeState == null) // todo, handle this properly + return DECOMMISSIONED; + switch (nodeState) + { + case REGISTERED: + return NEEDS_BOOTSTRAP; + case BOOTSTRAPPING: + case BOOT_REPLACING: + return IN_PROGRESS; + case JOINED: + case LEAVING: + case MOVING: + return COMPLETED; + case LEFT: + default: + return DECOMMISSIONED; + } + } } public static void persistLocalMetadata() - { - persistLocalMetadata(UUID::randomUUID); - } - - @VisibleForTesting - public static void persistLocalMetadata(Supplier nodeIdSupplier) { String req = "INSERT INTO system.%s (" + "key," + @@ -610,13 +686,6 @@ public final class SystemKeyspace DatabaseDescriptor.getStoragePort(), FBUtilities.getJustLocalAddress(), DatabaseDescriptor.getStoragePort()); - - // We should store host ID as soon as possible in the system.local table and flush that table to disk so that - // we can be sure that those changes are stored in sstable and not in the commit log (see CASSANDRA-18153). - // It is very unlikely that when upgrading the host id is not flushed to disk, but if that's the case, we limit - // this change only to the new installations or the user should just flush system.local table. - if (!CommitLog.instance.hasFilesToReplay()) - SystemKeyspace.getOrInitializeLocalHostId(nodeIdSupplier); } public static void updateCompactionHistory(TimeUUID taskId, @@ -831,7 +900,10 @@ public final class SystemKeyspace public static synchronized void updateTokens(InetAddressAndPort ep, Collection tokens) { if (ep.equals(FBUtilities.getBroadcastAddressAndPort())) + { + updateLocalTokens(tokens); return; + } String req = "INSERT INTO system.%s (peer, tokens) VALUES (?, ?)"; executeInternal(String.format(req, LEGACY_PEERS), ep.getAddress(), tokensAsSet(tokens)); @@ -895,11 +967,11 @@ public final class SystemKeyspace executeInternal(format(req, LOCAL, LOCAL), version); } - private static Set tokensAsSet(Collection tokens) + public static Set tokensAsSet(Collection tokens) { if (tokens.isEmpty()) return Collections.emptySet(); - Token.TokenFactory factory = StorageService.instance.getTokenFactory(); + Token.TokenFactory factory = ClusterMetadata.current().partitioner.getTokenFactory(); Set s = new HashSet<>(tokens.size()); for (Token tk : tokens) s.add(factory.toString(tk)); @@ -908,7 +980,7 @@ public final class SystemKeyspace private static Collection deserializeTokens(Collection tokensStrings) { - Token.TokenFactory factory = StorageService.instance.getTokenFactory(); + Token.TokenFactory factory = ClusterMetadata.current().partitioner.getTokenFactory(); List tokens = new ArrayList<>(tokensStrings.size()); for (String tk : tokensStrings) tokens.add(factory.fromString(tk)); @@ -928,9 +1000,10 @@ public final class SystemKeyspace } /** + * * This method is used to update the System Keyspace with the new tokens for this node */ - public static synchronized void updateTokens(Collection tokens) + public static synchronized void updateLocalTokens(Collection tokens) { assert !tokens.isEmpty() : "removeEndpoint should be used instead"; @@ -1240,27 +1313,6 @@ public final class SystemKeyspace return null; } - /** - * Read the host ID from the system keyspace, creating (and storing) one if - * none exists. - */ - public static synchronized UUID getOrInitializeLocalHostId() - { - return getOrInitializeLocalHostId(UUID::randomUUID); - } - - private static synchronized UUID getOrInitializeLocalHostId(Supplier nodeIdSupplier) - { - UUID hostId = getLocalHostId(); - if (hostId != null) - return hostId; - - // ID not found, generate a new one, persist, and then return it. - hostId = nodeIdSupplier.get(); - logger.warn("No host ID found, created {} (Note: This should happen exactly once per node).", hostId); - return setLocalHostId(hostId); - } - /** * Sets the local host ID explicitly. Should only be called outside of SystemTable when replacing a node. */ @@ -1316,6 +1368,20 @@ public final class SystemKeyspace return null; } + public static Set allKnownDatacenters() + { + Set dcs = new HashSet<>(); + dcs.add(getDatacenter()); + String req = "SELECT data_center FROM system.%s"; + UntypedResultSet result = executeInternal(format(req, PEERS_V2)); + if (result != null) + { + for (UntypedResultSet.Row row : result) + dcs.add(row.getString("data_center")); + } + return dcs; + } + /** * Load the current paxos state for the table and key */ @@ -1937,4 +2003,122 @@ public final class SystemKeyspace return TopPartitionTracker.StoredTopPartitions.EMPTY; } } + + public static void storeSnapshot(Epoch epoch, long period, ByteBuffer snapshot) + { + logger.info("Storing snapshot of cluster metadata at epoch {} (period {})", epoch, period); + String query = String.format("INSERT INTO %s.%s (epoch, period, snapshot) VALUES (?, ?, ?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); + executeInternal(query, epoch.getEpoch(), period, snapshot); + } + + public static ByteBuffer getSnapshot(Epoch epoch) + { + logger.info("Getting snapshot of epoch = {}", epoch); + String query = String.format("SELECT SNAPSHOT FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); + UntypedResultSet res = executeInternal(query, epoch.getEpoch()); + if (res == null || res.isEmpty()) + return null; + return res.one().getBytes("snapshot").duplicate(); + } + + public static Sealed findSealedPeriodForEpochScan(Epoch search) + { + String query = String.format("SELECT max_epoch, period FROM %s.%s WHERE max_epoch >= ? LIMIT 1 ALLOW FILTERING", SchemaConstants.SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME); + UntypedResultSet res = executeInternal(query, search.getEpoch()); + if (res != null && !res.isEmpty()) + { + long period = res.one().getLong("period"); + long epoch = res.one().getLong("max_epoch"); + return new Sealed(period, epoch); + } + + // nothing found for this epoch, is the table empty or is the search epoch > the maximum + query = String.format("SELECT max_epoch, period FROM %s.%s LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME); + res = executeInternal(query); + // table is empty, so any scan for the epoch will have to begin at Period.EMPTY + if (res == null || res.isEmpty()) + return Sealed.EMPTY; + + // the index table has some data, but is the search target greater than the max epoch in last sealed period? + // This query is relatively costly, so we do it last. Retain the min period/epoch that we did find in the + // previous query just in case we need them + // TODO add a nodetool command to rebuild the local sealed periods table + long lowestPeriod = res.one().getLong("period"); + long lowestMaxEpoch = res.one().getLong("max_epoch"); + logger.info("Scanning sealed periods by epoch table, this may be an expensive operation and the index table {} should be rebuilt", SEALED_PERIODS_TABLE_NAME); + query = String.format("SELECT max(max_epoch) AS max_epoch FROM %s.%s LIMIT 1 ALLOW FILTERING;", SchemaConstants.SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME); + res = executeInternal(query); + + // should never happen because the previous query returned the min, but just in case the table has been + // truncated since then, return the min Sealed. + if (res == null || res.isEmpty()) + return new Sealed(lowestPeriod, lowestMaxEpoch); + + // use the max epoch to look up the sealed period + long maxEpoch = res.one().getLong("max_epoch"); + query = String.format("SELECT period FROM %s.%s WHERE max_epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME); + res = executeInternal(query, maxEpoch); + if (res == null || res.isEmpty()) + return new Sealed(lowestPeriod, lowestMaxEpoch); + // this is the last recorded sealed period *before* the target epoch, so any scan should start at the + // *next* period, so we bump both period and epoch by 1 + long maxPeriod = res.one().getLong("period"); + return new Sealed(maxPeriod + 1, maxEpoch + 1); + } + + public static Sealed getLastSealedPeriod() + { + String query = String.format("SELECT epoch, period FROM %s.%s WHERE key = 'latest'", SchemaConstants.SYSTEM_KEYSPACE_NAME, LAST_SEALED_PERIOD_TABLE_NAME); + UntypedResultSet res = executeInternal(query); + if (res == null || res.isEmpty()) + return Sealed.EMPTY; + long epoch = res.one().getLong("epoch"); + long period = res.one().getLong("period"); + return new Sealed(period, Epoch.create(epoch)); + } + + public static void sealPeriod(long period, Epoch epoch) + { + String query = String.format("INSERT INTO %s.%s (max_epoch, period) VALUES (?,?)", SchemaConstants.SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME); + executeInternal(query, epoch.getEpoch(), period); + query = String.format("UPDATE %s.%s SET period = ?, epoch = ? WHERE key = 'latest'", SchemaConstants.SYSTEM_KEYSPACE_NAME, LAST_SEALED_PERIOD_TABLE_NAME); + executeInternal(query, period, epoch.getEpoch()); + } + + public static Map peerEndpointStates() + { + Map epstates = new HashMap<>(); + VersionedValue.VersionedValueFactory vf = StorageService.instance.valueFactory; + String query = String.format("select * from %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, PEERS_V2); + UntypedResultSet res = executeInternal(query); + for (UntypedResultSet.Row row : res) + { + EndpointState epstate = new EndpointState(new HeartBeatState(0, 0)); + InetAddressAndPort endpoint = InetAddressAndPort.getByAddressOverrideDefaults(row.getInetAddress("peer"), row.getInt("peer_port")); + epstate.addApplicationState(DC, vf.datacenter(row.getString("data_center"))); + epstate.addApplicationState(RACK, vf.rack(row.getString("rack"))); + epstate.addApplicationState(RELEASE_VERSION, vf.releaseVersion(row.getString("release_version"))); + epstate.addApplicationState(HOST_ID, vf.hostId(row.getUUID("host_id"))); + Collection tokens = deserializeTokens(row.getSet("tokens", UTF8Type.instance)); + epstate.addApplicationState(STATUS_WITH_PORT, vf.normal(tokens)); + epstate.addApplicationState(TOKENS, vf.tokens(tokens)); + + if (row.has("preferred_ip")) + { + epstate.addApplicationState(INTERNAL_ADDRESS_AND_PORT, + vf.internalAddressAndPort(InetAddressAndPort.getByAddressOverrideDefaults(row.getInetAddress("preferred_ip"), + row.getInt("preferred_port")))); + } + + if (row.has("native_ip")) + { + epstate.addApplicationState(NATIVE_ADDRESS_AND_PORT, + vf.nativeaddressAndPort(InetAddressAndPort.getByAddressOverrideDefaults(row.getInetAddress("native_ip"), + row.getInt("native_port")))); + } + + epstates.put(endpoint, epstate); + } + return epstates; + } } diff --git a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java index e6cc2fa814..e5e82f9662 100644 --- a/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java +++ b/src/java/org/apache/cassandra/db/commitlog/AbstractCommitLogSegmentManager.java @@ -320,11 +320,12 @@ public abstract class AbstractCommitLogSegmentManager void forceRecycleAll(Collection droppedTables) { List segmentsToRecycle = new ArrayList<>(activeSegments); - CommitLogSegment last = segmentsToRecycle.get(segmentsToRecycle.size() - 1); + CommitLogSegment last = segmentsToRecycle.isEmpty() ? null : segmentsToRecycle.get(segmentsToRecycle.size() - 1); advanceAllocatingFrom(last); // wait for the commit log modifications - last.waitForModifications(); + if (last != null) + last.waitForModifications(); // make sure the writes have materialized inside of the memtables by waiting for all outstanding writes // to complete @@ -350,7 +351,7 @@ public abstract class AbstractCommitLogSegmentManager } CommitLogSegment first; - if ((first = activeSegments.peek()) != null && first.id <= last.id) + if ((first = activeSegments.peek()) != null && last != null && first.id <= last.id) logger.error("Failed to force-recycle all segments; at least one segment is still in use with dirty CFs."); } catch (Throwable t) diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java index 0eeed14be6..f228540394 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionManager.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionManager.java @@ -96,6 +96,7 @@ import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.metrics.CompactionMetrics; import org.apache.cassandra.metrics.TableMetrics; @@ -113,10 +114,13 @@ import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.WrappedRunnable; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.ImmediateFuture; import org.apache.cassandra.utils.concurrent.Refs; + import static java.util.Collections.singleton; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; import static org.apache.cassandra.concurrent.FutureTask.callable; @@ -617,13 +621,26 @@ public class CompactionManager implements CompactionManagerMBean, ICompactionMan assert !cfStore.isIndex(); Keyspace keyspace = cfStore.keyspace; - // if local ranges is empty, it means no data should remain - final RangesAtEndpoint replicas = StorageService.instance.getLocalReplicas(keyspace.getName()); - final Set> allRanges = replicas.ranges(); - final Set> transientRanges = replicas.onlyTransient().ranges(); - final Set> fullRanges = replicas.onlyFull().ranges(); + if (!StorageService.instance.isJoined()) + { + logger.info("Cleanup cannot run before a node has joined the ring"); + return AllSSTableOpStatus.ABORTED; + } + if (cfStore.keyspace.getMetadata().params.replication.isMeta()) + return AllSSTableOpStatus.SUCCESSFUL; // todo - we probably want to be able to cleanup MetaStrategy keyspaces final boolean hasIndexes = cfStore.indexManager.hasIndexes(); + // if local ranges is empty, it means no data should remain + // we only consider write placements during cleanup as range movements always ensure + // overlap between new replicas accepting reads and old replicas accepting writes + ClusterMetadata cm = ClusterMetadata.current(); + DataPlacement placement = cm.placements.get(keyspace.getMetadata().params.replication); + InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); + RangesAtEndpoint localWrites = placement.writes.byEndpoint().get(local); + final Set> allRanges = new HashSet<>(localWrites.ranges()); + final Set> transientRanges = new HashSet<>(localWrites.onlyTransient().ranges()); + final Set> fullRanges = new HashSet<>(localWrites.onlyFull().ranges()); + return parallelAllSSTableOperation(cfStore, new OneSSTableOperation() { @Override @@ -665,7 +682,7 @@ public class CompactionManager implements CompactionManagerMBean, ICompactionMan public void execute(LifecycleTransaction txn) throws IOException { CleanupStrategy cleanupStrategy = CleanupStrategy.get(cfStore, allRanges, transientRanges, txn.onlyOne().isRepaired(), FBUtilities.nowInSeconds()); - doCleanupOne(cfStore, txn, cleanupStrategy, replicas.ranges(), hasIndexes); + doCleanupOne(cfStore, txn, cleanupStrategy, allRanges, hasIndexes); } }, jobs, OperationType.CLEANUP); } @@ -1291,7 +1308,7 @@ public class CompactionManager implements CompactionManagerMBean, ICompactionMan /* Used in tests. */ public void disableAutoCompaction() { - for (String ksname : Schema.instance.distributedKeyspaces().names()) + for (String ksname : Schema.instance.getKeyspaces()) { for (ColumnFamilyStore cfs : Keyspace.open(ksname).getColumnFamilyStores()) cfs.disableAutoCompaction(); diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java index dfcd9aee4e..c31a9b2259 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java @@ -70,6 +70,7 @@ import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.io.util.File; import org.apache.cassandra.notifications.INotification; import org.apache.cassandra.notifications.INotificationConsumer; +import org.apache.cassandra.notifications.InitialSSTableAddedNotification; import org.apache.cassandra.notifications.SSTableAddedNotification; import org.apache.cassandra.notifications.SSTableDeletingNotification; import org.apache.cassandra.notifications.SSTableListChangedNotification; @@ -890,6 +891,11 @@ public class CompactionStrategyManager implements INotificationConsumer SSTableAddedNotification flushedNotification = (SSTableAddedNotification) notification; handleFlushNotification(flushedNotification.added); } + else if (notification instanceof InitialSSTableAddedNotification) + { + InitialSSTableAddedNotification flushedNotification = (InitialSSTableAddedNotification) notification; + handleFlushNotification(flushedNotification.added); + } else if (notification instanceof SSTableListChangedNotification) { SSTableListChangedNotification listChangedNotification = (SSTableListChangedNotification) notification; diff --git a/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java b/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java index 6174612a94..0b89111ccd 100644 --- a/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java +++ b/src/java/org/apache/cassandra/db/compaction/ShardManagerNoDisks.java @@ -55,8 +55,8 @@ public class ShardManagerNoDisks implements ShardManager public boolean isOutOfDate(long ringVersion) { - return ringVersion != localRanges.ringVersion && - localRanges.ringVersion != ColumnFamilyStore.RING_VERSION_IRRELEVANT; + return ringVersion != localRanges.ringVersion.getEpoch() && + !localRanges.ringVersion.is(ColumnFamilyStore.RING_VERSION_IRRELEVANT); } @Override diff --git a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java index 2539c21b8e..0cb1f46cc8 100644 --- a/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.java @@ -52,7 +52,7 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableMultiWriter; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Overlaps; @@ -295,13 +295,14 @@ public class UnifiedCompactionStrategy extends AbstractCompactionStrategy private void maybeUpdateShardManager() { - if (shardManager != null && !shardManager.isOutOfDate(StorageService.instance.getTokenMetadata().getRingVersion())) + // TODO - modify ShardManager::isOutOfDate to take an Epoch + if (shardManager != null && !shardManager.isOutOfDate(ClusterMetadata.current().epoch.getEpoch())) return; // the disk boundaries (and thus the local ranges too) have not changed since the last time we calculated synchronized (this) { // Recheck after entering critical section, another thread may have beaten us to it. - while (shardManager == null || shardManager.isOutOfDate(StorageService.instance.getTokenMetadata().getRingVersion())) + while (shardManager == null || shardManager.isOutOfDate(ClusterMetadata.current().epoch.getEpoch())) shardManager = ShardManager.create(cfs); // Note: this can just as well be done without the synchronization (races would be benign, just doing some // redundant work). For the current usages of this blocking is fine and expected to perform no worse. diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java index cefd941622..da9bc61271 100644 --- a/src/java/org/apache/cassandra/db/filter/RowFilter.java +++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java @@ -65,6 +65,7 @@ public abstract class RowFilter implements Iterable private static final Logger logger = LoggerFactory.getLogger(RowFilter.class); public static final Serializer serializer = new Serializer(); + public static final RowFilter NONE = new CQLFilter(Collections.emptyList()); protected final List expressions; diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java index 0f6831cbbc..b9d21a412d 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java @@ -95,7 +95,7 @@ public abstract class Guardrail */ public boolean enabled(@Nullable ClientState state) { - return DatabaseDescriptor.isDaemonInitialized() && (state == null || state.isOrdinaryUser()); + return DatabaseDescriptor.isDaemonInitialized() && (state == null || (state.isOrdinaryUser() && state.applyGuardrails())); } protected void warn(String message) diff --git a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java index e959c72fa9..5decf00384 100644 --- a/src/java/org/apache/cassandra/db/lifecycle/Tracker.java +++ b/src/java/org/apache/cassandra/db/lifecycle/Tracker.java @@ -230,9 +230,15 @@ public class Tracker addSSTablesInternal(sstables, true, false, true); } - public void addInitialSSTablesWithoutUpdatingSize(Iterable sstables) + public void addInitialSSTablesWithoutUpdatingSize(Iterable sstables, ColumnFamilyStore cfs) { - addSSTablesInternal(sstables, true, false, false); + if (!isDummy()) + { + for (SSTableReader reader : sstables) + reader.setupOnline(); + } + apply(updateLiveSet(emptySet(), sstables)); + notifyAdded(sstables, true); } public void updateInitialSSTableSize(Iterable sstables) diff --git a/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java b/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java index 8526dace39..b431d360ed 100644 --- a/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java +++ b/src/java/org/apache/cassandra/db/memtable/AbstractAllocatorMemtable.java @@ -32,6 +32,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ClusteringComparator; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.commitlog.CommitLogPosition; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; @@ -128,13 +129,13 @@ public abstract class AbstractAllocatorMemtable extends AbstractMemtableWithComm } @Override - public boolean shouldSwitch(ColumnFamilyStore.FlushReason reason) + public boolean shouldSwitch(ColumnFamilyStore.FlushReason reason, TableMetadata latest) { switch (reason) { case SCHEMA_CHANGE: - return initialComparator != metadata().comparator // If the CF comparator has changed, because our partitions reference the old one - || !initialFactory.equals(metadata().params.memtable.factory()); // If a different type of memtable is requested + return initialComparator != latest.comparator // If the CF comparator has changed, because our partitions reference the old one + || !initialFactory.equals(latest.params.memtable.factory()); // If a different type of memtable is requested case OWNED_RANGES_CHANGE: return false; // by default we don't use the local ranges, thus this has no effect default: diff --git a/src/java/org/apache/cassandra/db/memtable/Memtable.java b/src/java/org/apache/cassandra/db/memtable/Memtable.java index 5ce59f6191..06dfe19976 100644 --- a/src/java/org/apache/cassandra/db/memtable/Memtable.java +++ b/src/java/org/apache/cassandra/db/memtable/Memtable.java @@ -399,10 +399,19 @@ public interface Memtable extends Comparable, UnfilteredSource * - SNAPSHOT will be followed by performSnapshot(). * - STREAMING/REPAIR will be followed by creating a FlushSet for the streamed/repaired ranges. This data will be * used to create sstables, which will be streamed and then deleted. + * The table metadata is supplied explicitly as this might not be the same as the current published metadata for + * the table. When applying a schema change, the ColumnFamilyStore instance is reloaded using the new table metadata + * before the Schema registry is updated. The memtable needs to examine the new metadata in order to determine + * whether the changes warrant a switch. * This will not be called to perform truncation or drop (in that case the memtable is unconditionally dropped), * but a flush may nevertheless be requested in that case to prepare a snapshot. */ - boolean shouldSwitch(ColumnFamilyStore.FlushReason reason); + boolean shouldSwitch(ColumnFamilyStore.FlushReason reason, TableMetadata latest); + + default boolean shouldSwitch(ColumnFamilyStore.FlushReason reason) + { + return shouldSwitch(reason, metadata()); + } /** * Called when the table's metadata is updated. The memtable's metadata reference now points to the new version. diff --git a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java index 864899f6a4..5412c2cf62 100644 --- a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java +++ b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java @@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.tcm.Epoch; /** * Holds boundaries (tokens) used to map a particular token (so partition key) to a shard id. @@ -43,21 +44,21 @@ public class ShardBoundaries // - there is only 1 shard configured // - the default partitioner doesn't support splitting // - the keyspace is local system keyspace - public static final ShardBoundaries NONE = new ShardBoundaries(EMPTY_TOKEN_ARRAY, -1); + public static final ShardBoundaries NONE = new ShardBoundaries(EMPTY_TOKEN_ARRAY, Epoch.EMPTY); private final Token[] boundaries; - public final long ringVersion; + public final Epoch epoch; @VisibleForTesting - public ShardBoundaries(Token[] boundaries, long ringVersion) + public ShardBoundaries(Token[] boundaries, Epoch epoch) { this.boundaries = boundaries; - this.ringVersion = ringVersion; + this.epoch = epoch; } - public ShardBoundaries(List boundaries, long ringVersion) + public ShardBoundaries(List boundaries, Epoch epoch) { - this(boundaries.toArray(EMPTY_TOKEN_ARRAY), ringVersion); + this(boundaries.toArray(EMPTY_TOKEN_ARRAY), epoch); } /** diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java index 1047fc0a70..9f25468495 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java @@ -35,11 +35,17 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.db.*; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.exceptions.CoordinatorBehindException; +import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; @@ -73,10 +79,12 @@ public class PartitionUpdate extends AbstractBTreePartition private final BTreePartitionData holder; private final DeletionInfo deletionInfo; private final TableMetadata metadata; + public final Epoch serializedAtEpoch; private final boolean canHaveShadowedData; private PartitionUpdate(TableMetadata metadata, + Epoch serializedAtEpoch, DecoratedKey key, BTreePartitionData holder, MutableDeletionInfo deletionInfo, @@ -87,6 +95,7 @@ public class PartitionUpdate extends AbstractBTreePartition this.holder = holder; this.deletionInfo = deletionInfo; this.canHaveShadowedData = canHaveShadowedData; + this.serializedAtEpoch = serializedAtEpoch; } /** @@ -101,7 +110,7 @@ public class PartitionUpdate extends AbstractBTreePartition { MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); + return new PartitionUpdate(metadata, metadata.epoch, key, holder, deletionInfo, false); } /** @@ -118,7 +127,7 @@ public class PartitionUpdate extends AbstractBTreePartition { MutableDeletionInfo deletionInfo = new MutableDeletionInfo(timestamp, nowInSec); BTreePartitionData holder = new BTreePartitionData(RegularAndStaticColumns.NONE, BTree.empty(), deletionInfo, Rows.EMPTY_STATIC_ROW, EncodingStats.NO_STATS); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); + return new PartitionUpdate(metadata, metadata.epoch, key, holder, deletionInfo, false); } /** @@ -144,7 +153,7 @@ public class PartitionUpdate extends AbstractBTreePartition staticRow == null ? Rows.EMPTY_STATIC_ROW : staticRow, EncodingStats.NO_STATS ); - return new PartitionUpdate(metadata, key, holder, deletionInfo, false); + return new PartitionUpdate(metadata, metadata.epoch, key, holder, deletionInfo, false); } /** @@ -191,7 +200,7 @@ public class PartitionUpdate extends AbstractBTreePartition iterator = UnfilteredRowIterators.withOnlyQueriedData(iterator, filter); BTreePartitionData holder = build(iterator, 16); MutableDeletionInfo deletionInfo = (MutableDeletionInfo) holder.deletionInfo; - return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false); + return new PartitionUpdate(iterator.metadata(), iterator.metadata().epoch, iterator.partitionKey(), holder, deletionInfo, false); } /** @@ -210,7 +219,7 @@ public class PartitionUpdate extends AbstractBTreePartition iterator = RowIterators.withOnlyQueriedData(iterator, filter); MutableDeletionInfo deletionInfo = MutableDeletionInfo.live(); BTreePartitionData holder = build(iterator, deletionInfo, true); - return new PartitionUpdate(iterator.metadata(), iterator.partitionKey(), holder, deletionInfo, false); + return new PartitionUpdate(iterator.metadata(), iterator.metadata().epoch, iterator.partitionKey(), holder, deletionInfo, false); } @@ -223,7 +232,7 @@ public class PartitionUpdate extends AbstractBTreePartition columnSet.add(column.column()); RegularAndStaticColumns columns = RegularAndStaticColumns.builder().addAll(columnSet).build(); - return new PartitionUpdate(this.metadata, this.partitionKey, this.holder.withColumns(columns), this.deletionInfo.mutableCopy(), false); + return new PartitionUpdate(this.metadata, this.metadata.epoch, this.partitionKey, this.holder.withColumns(columns), this.deletionInfo.mutableCopy(), false); } @@ -465,7 +474,7 @@ public class PartitionUpdate extends AbstractBTreePartition /** * - * @return the estimated number of rows affected by this mutation + * @return the estimated number of rows affected by this mutation */ public int affectedRowCount() { @@ -506,7 +515,7 @@ public class PartitionUpdate extends AbstractBTreePartition for (Row row : this) { if (row.deletion().isLive()) - // If the row is live, this will include simple tombstones as well as cells w/ actual data. + // If the row is live, this will include simple tombstones as well as cells w/ actual data. count += row.columnCount(); else // We have a row deletion, so account for the columns that might be deleted. @@ -539,7 +548,14 @@ public class PartitionUpdate extends AbstractBTreePartition */ public static SimpleBuilder simpleBuilder(TableMetadata metadata, Object... partitionKeyValues) { - return new SimpleBuilders.PartitionUpdateBuilder(metadata, partitionKeyValues); + // Here we dereference the current version of the supplied TableMetadata. The reason for this is that in some + // places we still reference static TableMetadata instances. + // For instance, TraceKeyspace contains Sessions & Events static members which are created at startup when the + // current epoch is Epoch.EMPTY. These are used to construct mutations when tracing is enabled and when the + // mutations are serialised and sent between replica & coordinator the epoch comparisons in PartitionUpdate + // deserializer trigger an IncompatibleSchemaException. + // TODO ultimately remove the use of static TableMetadata instances in System/Tracing/Auth keyspaces. + return new SimpleBuilders.PartitionUpdateBuilder(metadata.ref.get(), partitionKeyValues); } public void validateIndexedColumns() @@ -554,7 +570,7 @@ public class PartitionUpdate extends AbstractBTreePartition MutableDeletionInfo deletionInfo, boolean canHaveShadowedData) { - return new PartitionUpdate(metadata, key, holder, deletionInfo, canHaveShadowedData); + return new PartitionUpdate(metadata, metadata.epoch, key, holder, deletionInfo, canHaveShadowedData); } /** @@ -713,24 +729,45 @@ public class PartitionUpdate extends AbstractBTreePartition assert !iter.isReverseOrder(); update.metadata.id.serialize(out); + if (version >= MessagingService.VERSION_50) + Epoch.serializer.serialize(update.metadata.epoch != null ? update.metadata.epoch : Epoch.EMPTY, out); UnfilteredRowIteratorSerializer.serializer.serialize(iter, null, out, version, update.rowCount()); } } public PartitionUpdate deserialize(DataInputPlus in, int version, DeserializationHelper.Flag flag) throws IOException { - TableMetadata metadata = Schema.instance.getExistingTableMetadata(TableId.deserialize(in)); - UnfilteredRowIteratorSerializer.Header header = UnfilteredRowIteratorSerializer.serializer.deserializeHeader(metadata, null, in, version, flag); + TableId tableId = TableId.deserialize(in); + Epoch remoteVersion = null; + if (version >= MessagingService.VERSION_50) + remoteVersion = Epoch.serializer.deserialize(in); + TableMetadata tableMetadata; + try + { + tableMetadata = Schema.instance.getExistingTableMetadata(tableId); + } + catch (UnknownTableException e) + { + ClusterMetadata metadata = ClusterMetadata.current(); + Epoch localCurrentEpoch = metadata.epoch; + if (remoteVersion != null && localCurrentEpoch.isAfter(remoteVersion)) + { + TCMMetrics.instance.coordinatorBehindSchema.mark(); + throw new CoordinatorBehindException(e.getMessage(), e); + } + throw e; + } + UnfilteredRowIteratorSerializer.Header header = UnfilteredRowIteratorSerializer.serializer.deserializeHeader(tableMetadata, null, in, version, flag); if (header.isEmpty) - return emptyUpdate(metadata, header.key); + return emptyUpdate(tableMetadata, header.key); assert !header.isReversed; assert header.rowEstimate >= 0; - MutableDeletionInfo.Builder deletionBuilder = MutableDeletionInfo.builder(header.partitionDeletion, metadata.comparator, false); + MutableDeletionInfo.Builder deletionBuilder = MutableDeletionInfo.builder(header.partitionDeletion, tableMetadata.comparator, false); Object[] rows; try (BTree.FastBuilder builder = BTree.fastBuilder(); - UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, flag, header)) + UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, tableMetadata, flag, header)) { while (partition.hasNext()) { @@ -744,19 +781,27 @@ public class PartitionUpdate extends AbstractBTreePartition } MutableDeletionInfo deletionInfo = deletionBuilder.build(); - return new PartitionUpdate(metadata, + return new PartitionUpdate(tableMetadata, + remoteVersion, header.key, new BTreePartitionData(header.sHeader.columns(), rows, deletionInfo, header.staticRow, header.sHeader.stats()), deletionInfo, false); } - public static boolean isEmpty(ByteBuffer in, DeserializationHelper.Flag flag, DecoratedKey key) throws IOException + public static boolean isEmpty(ByteBuffer in, DeserializationHelper.Flag flag, DecoratedKey key, int version) throws IOException { int position = in.position(); position += 16; // CFMetaData.serializer.deserialize(in, version); if (position >= in.limit()) throw new EOFException(); + + if (version >= MessagingService.VERSION_50) + { + long epoch = VIntCoding.getUnsignedVInt(in, position); + position += VIntCoding.computeVIntSize(epoch); + } + // DecoratedKey key = metadata.decorateKey(ByteBufferUtil.readWithVIntLength(in)); int keyLength = VIntCoding.getUnsignedVInt32(in, position); position += keyLength + VIntCoding.computeUnsignedVIntSize(keyLength); @@ -771,6 +816,7 @@ public class PartitionUpdate extends AbstractBTreePartition try (UnfilteredRowIterator iter = update.unfilteredIterator()) { return update.metadata.id.serializedSize() + + (version >= MessagingService.VERSION_50 ? Epoch.serializer.serializedSize(update.metadata.epoch) : 0) + UnfilteredRowIteratorSerializer.serializer.serializedSize(iter, null, version, update.rowCount()); } } @@ -964,6 +1010,7 @@ public class PartitionUpdate extends AbstractBTreePartition isBuilt = true; return new PartitionUpdate(metadata, + metadata.epoch, partitionKey(), new BTreePartitionData(columns, merged, diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamReader.java b/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamReader.java index 0e0fcaa960..db5fb25373 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamReader.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraCompressedStreamReader.java @@ -74,8 +74,8 @@ public class CassandraCompressedStreamReader extends CassandraStreamReader try (CompressedInputStream cis = new CompressedInputStream(inputPlus, compressionInfo, ChecksumType.CRC32, cfs::getCrcCheckChance)) { TrackedDataInputPlus in = new TrackedDataInputPlus(cis); - deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata())); writer = createWriter(cfs, totalSize, repairedAt, pendingRepair, inputVersion.format); + deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata()), session, writer); String filename = writer.getFilename(); String sectionName = filename + '-' + fileSeqNum; int sectionIdx = 0; diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java index 9819d4cf27..af4b4dbc18 100644 --- a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java +++ b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReader.java @@ -20,6 +20,9 @@ package org.apache.cassandra.db.streaming; import java.io.IOError; import java.io.IOException; import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.TimeUnit; import com.google.common.base.Preconditions; import com.google.common.collect.UnmodifiableIterator; @@ -38,6 +41,8 @@ import org.apache.cassandra.db.rows.EncodingStats; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.UnknownColumnException; import org.apache.cassandra.io.sstable.RangeAwareSSTableWriter; import org.apache.cassandra.io.sstable.SSTableMultiWriter; @@ -47,15 +52,19 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.TrackedDataInputPlus; +import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.ProgressInfo; +import org.apache.cassandra.streaming.StreamReceivedOutOfTokenRangeException; import org.apache.cassandra.streaming.StreamReceiver; import org.apache.cassandra.streaming.StreamSession; import org.apache.cassandra.streaming.compress.StreamCompressionInputStream; import org.apache.cassandra.streaming.messages.StreamMessageHeader; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.TimeUUID; import static org.apache.cassandra.net.MessagingService.current_version; @@ -66,6 +75,7 @@ import static org.apache.cassandra.net.MessagingService.current_version; public class CassandraStreamReader implements IStreamReader { private static final Logger logger = LoggerFactory.getLogger(CassandraStreamReader.class); + private static final String logMessageTemplate = "[Stream #{}] Received streamed SSTable {} from {} containing key outside valid ranges {}"; protected final TableId tableId; protected final long estimatedKeys; protected final Collection sections; @@ -121,8 +131,8 @@ public class CassandraStreamReader implements IStreamReader try (StreamCompressionInputStream streamCompressionInputStream = new StreamCompressionInputStream(inputPlus, current_version)) { TrackedDataInputPlus in = new TrackedDataInputPlus(streamCompressionInputStream); - deserializer = new StreamDeserializer(cfs.metadata(), in, inputVersion, getHeader(cfs.metadata())); writer = createWriter(cfs, totalSize, repairedAt, pendingRepair, inputVersion.format); + deserializer = getDeserializer(cfs.metadata(), in, inputVersion, session, writer); String sequenceName = writer.getFilename() + '-' + fileSeqNum; long lastBytesRead = 0; while (in.getBytesRead() < totalSize) @@ -149,6 +159,15 @@ public class CassandraStreamReader implements IStreamReader } } + protected StreamDeserializer getDeserializer(TableMetadata metadata, + TrackedDataInputPlus in, + Version inputVersion, + StreamSession session, + SSTableMultiWriter writer) throws IOException + { + return new StreamDeserializer(metadata, in, inputVersion, getHeader(metadata), session, writer); + } + protected SerializationHeader getHeader(TableMetadata metadata) throws UnknownColumnException { return header != null? header.toHeader(metadata) : null; //pre-3.0 sstable have no SerializationHeader @@ -188,29 +207,52 @@ public class CassandraStreamReader implements IStreamReader private final SerializationHeader header; private final DeserializationHelper helper; - private DecoratedKey key; - private DeletionTime partitionLevelDeletion; - private SSTableSimpleIterator iterator; - private Row staticRow; + private final List> ownedRanges; + private final StreamSession session; + private final SSTableMultiWriter writer; + + private int lastCheckedRangeIndex; + protected DecoratedKey key; + protected DeletionTime partitionLevelDeletion; + protected SSTableSimpleIterator iterator; + protected Row staticRow; private IOException exception; private Version version; - public StreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header) throws IOException + public StreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header, StreamSession session, SSTableMultiWriter writer) throws IOException { this.metadata = metadata; this.in = in; this.helper = new DeserializationHelper(metadata, version.correspondingMessagingVersion(), DeserializationHelper.Flag.PRESERVE_SIZE); this.header = header; this.version = version; + ownedRanges = Range.normalize(StorageService.instance.getLocalAndPendingRanges(metadata.keyspace)); + lastCheckedRangeIndex = 0; + + this.session = session; + this.writer = writer; } - public StreamDeserializer newPartition() throws IOException + public UnfilteredRowIterator newPartition() throws IOException + { + readKey(); + readPartition(); + return this; + } + + protected void readKey() throws IOException { key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in)); + lastCheckedRangeIndex = verifyKeyInOwnedRanges(key, + ownedRanges, + lastCheckedRangeIndex); + } + + protected void readPartition() throws IOException + { partitionLevelDeletion = DeletionTime.getSerializer(version).deserialize(in); iterator = SSTableSimpleIterator.create(metadata, in, header, helper, partitionLevelDeletion); staticRow = iterator.readStaticRow(); - return this; } public TableMetadata metadata() @@ -291,5 +333,27 @@ public class CassandraStreamReader implements IStreamReader public void close() { } + + private int verifyKeyInOwnedRanges(final DecoratedKey key, + List> ownedRanges, + int lastCheckedRangeIndex) + { + if (lastCheckedRangeIndex < ownedRanges.size()) + { + ListIterator> rangesToCheck = ownedRanges.listIterator(lastCheckedRangeIndex); + while (rangesToCheck.hasNext()) + { + Range range = rangesToCheck.next(); + if (range.contains(key.getToken())) + return lastCheckedRangeIndex; + + lastCheckedRangeIndex++; + } + } + + StorageMetrics.totalOpsForInvalidToken.inc(); + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, session.planId(), writer.getFilename(), session.peer, ownedRanges); + throw new StreamReceivedOutOfTokenRangeException(ownedRanges, key, writer.getFilename()); + } } } diff --git a/src/java/org/apache/cassandra/db/view/TableViews.java b/src/java/org/apache/cassandra/db/view/TableViews.java index 366d4278e0..f717e3ec85 100644 --- a/src/java/org/apache/cassandra/db/view/TableViews.java +++ b/src/java/org/apache/cassandra/db/view/TableViews.java @@ -17,7 +17,14 @@ */ package org.apache.cassandra.db.view; -import java.util.*; +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -27,14 +34,36 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionInfo; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.RangeTombstone; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.commitlog.CommitLogPosition; -import org.apache.cassandra.db.filter.*; -import org.apache.cassandra.db.partitions.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.filter.ClusteringIndexFilter; +import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.service.StorageProxy; @@ -57,9 +86,9 @@ public class TableViews extends AbstractCollection // list is the best option. private final List views = new CopyOnWriteArrayList(); - public TableViews(TableId id) + public TableViews(TableMetadata tableMetadata) { - baseTableMetadata = Schema.instance.getTableMetadataRef(id); + baseTableMetadata = tableMetadata.ref; } public boolean hasViews() diff --git a/src/java/org/apache/cassandra/db/view/ViewBuilder.java b/src/java/org/apache/cassandra/db/view/ViewBuilder.java index daedf48f29..c59cfec855 100644 --- a/src/java/org/apache/cassandra/db/view/ViewBuilder.java +++ b/src/java/org/apache/cassandra/db/view/ViewBuilder.java @@ -43,6 +43,7 @@ import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.locator.Replicas; import org.apache.cassandra.schema.SystemDistributedKeyspace; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Future; @@ -66,7 +67,7 @@ class ViewBuilder private final ColumnFamilyStore baseCfs; private final View view; private final String ksName; - private final UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId(); + private final UUID localHostId; private final Set> builtRanges = Sets.newConcurrentHashSet(); private final Map, Pair> pendingRanges = Maps.newConcurrentMap(); private final Set tasks = Sets.newConcurrentHashSet(); @@ -79,6 +80,7 @@ class ViewBuilder this.baseCfs = baseCfs; this.view = view; ksName = baseCfs.metadata.keyspace; + this.localHostId = ClusterMetadata.current().myNodeId().toUUID(); } public void start() diff --git a/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java b/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java index 7bda2fddac..0b53542c7d 100644 --- a/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java +++ b/src/java/org/apache/cassandra/db/view/ViewBuilderTask.java @@ -110,7 +110,7 @@ public class ViewBuilderTask extends CompactionInfo.Holder implements Callable> mutations = baseCfs.keyspace.viewManager - .forTable(baseCfs.metadata.id) + .forTable(baseCfs.metadata.get()) .generateViewUpdates(Collections.singleton(view), data, empty, nowInSec, true); AtomicLong noBase = new AtomicLong(Long.MAX_VALUE); diff --git a/src/java/org/apache/cassandra/db/view/ViewManager.java b/src/java/org/apache/cassandra/db/view/ViewManager.java index 106a15fdd9..cf6b916e05 100644 --- a/src/java/org/apache/cassandra/db/view/ViewManager.java +++ b/src/java/org/apache/cassandra/db/view/ViewManager.java @@ -28,12 +28,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.schema.ViewMetadata; +import org.apache.cassandra.schema.*; import org.apache.cassandra.db.*; import org.apache.cassandra.db.partitions.*; -import org.apache.cassandra.schema.SystemDistributedKeyspace; -import org.apache.cassandra.schema.Views; import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.config.CassandraRelevantProperties.MV_ENABLE_COORDINATOR_BATCHLOG; @@ -84,7 +81,7 @@ public class ViewManager if (coordinatorBatchlog && keyspace.getReplicationStrategy().getReplicationFactor().allReplicas == 1) continue; - if (!forTable(update.metadata().id).updatedViews(update).isEmpty()) + if (!forTable(update.metadata()).updatedViews(update).isEmpty()) return true; } } @@ -97,9 +94,9 @@ public class ViewManager return viewsByName.values(); } - public void reload(boolean buildAllViews) + public void reload(KeyspaceMetadata keyspaceMetadata) { - Views views = keyspace.getMetadata().views; + Views views = keyspaceMetadata.views; Map newViewsByName = Maps.newHashMapWithExpectedSize(views.size()); for (ViewMetadata definition : views) { @@ -111,10 +108,16 @@ public class ViewManager if (!viewsByName.containsKey(entry.getKey())) addView(entry.getValue()); } + } - if (!buildAllViews) - return; - + public void buildViews() + { + Views views = keyspace.getMetadata().views; + Map newViewsByName = Maps.newHashMapWithExpectedSize(views.size()); + for (ViewMetadata definition : views) + { + newViewsByName.put(definition.name(), definition); + } // Building views involves updating view build status in the system_distributed // keyspace and therefore it requires ring information. This check prevents builds // being submitted when Keyspaces are initialized during CassandraDaemon::setup as @@ -149,7 +152,7 @@ public class ViewManager } View view = new View(definition, keyspace.getColumnFamilyStore(definition.baseTableId)); - forTable(view.getDefinition().baseTableId).add(view); + forTable(keyspace.getMetadata().tables.getNullable(view.getDefinition().baseTableId)).add(view); viewsByName.put(definition.name(), view); } @@ -166,7 +169,7 @@ public class ViewManager return; view.stopBuild(); - forTable(view.getDefinition().baseTableId).removeByName(name); + forTable(view.getDefinition().baseTableMetadata()).removeByName(name); SystemKeyspace.setViewRemoved(keyspace.getName(), view.name); SystemDistributedKeyspace.setViewRemoved(keyspace.getName(), view.name); } @@ -182,13 +185,13 @@ public class ViewManager view.build(); } - public TableViews forTable(TableId id) + public TableViews forTable(TableMetadata metadata) { - TableViews views = viewsByBaseTable.get(id); + TableViews views = viewsByBaseTable.get(metadata.id); if (views == null) { - views = new TableViews(id); - TableViews previous = viewsByBaseTable.putIfAbsent(id, views); + views = new TableViews(metadata); + TableViews previous = viewsByBaseTable.putIfAbsent(metadata.id, views); if (previous != null) views = previous; } diff --git a/src/java/org/apache/cassandra/db/view/ViewUtils.java b/src/java/org/apache/cassandra/db/view/ViewUtils.java index 55a462c31b..b41150b7a5 100644 --- a/src/java/org/apache/cassandra/db/view/ViewUtils.java +++ b/src/java/org/apache/cassandra/db/view/ViewUtils.java @@ -24,10 +24,11 @@ import java.util.function.Predicate; import com.google.common.collect.Iterables; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.NetworkTopologyStrategy; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; public final class ViewUtils { @@ -57,11 +58,13 @@ public final class ViewUtils * * @return Optional.empty() if this method is called using a base token which does not belong to this replica */ - public static Optional getViewNaturalEndpoint(AbstractReplicationStrategy replicationStrategy, Token baseToken, Token viewToken) + public static Optional getViewNaturalEndpoint(ClusterMetadata metadata, String keyspace, Token baseToken, Token viewToken) { String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); - EndpointsForToken naturalBaseReplicas = replicationStrategy.getNaturalReplicasForToken(baseToken); - EndpointsForToken naturalViewReplicas = replicationStrategy.getNaturalReplicasForToken(viewToken); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspace); + + EndpointsForToken naturalBaseReplicas = metadata.placements.get(keyspaceMetadata.params.replication).reads.forToken(baseToken).get(); + EndpointsForToken naturalViewReplicas = metadata.placements.get(keyspaceMetadata.params.replication).reads.forToken(viewToken).get(); Optional localReplica = Iterables.tryFind(naturalViewReplicas, Replica::isSelf).toJavaUtil(); if (localReplica.isPresent()) @@ -69,7 +72,7 @@ public final class ViewUtils // We only select replicas from our own DC // TODO: this is poor encapsulation, leaking implementation details of replication strategy - Predicate isLocalDC = r -> !(replicationStrategy instanceof NetworkTopologyStrategy) + Predicate isLocalDC = r -> !(keyspaceMetadata.replicationStrategy instanceof NetworkTopologyStrategy) || DatabaseDescriptor.getEndpointSnitch().getDatacenter(r).equals(localDataCenter); // We have to remove any endpoint which is shared between the base and the view, as it will select itself @@ -84,7 +87,9 @@ public final class ViewUtils // The replication strategy will be the same for the base and the view, as they must belong to the same keyspace. // Since the same replication strategy is used, the same placement should be used and we should get the same // number of replicas for all of the tokens in the ring. - assert baseReplicas.size() == viewReplicas.size() : "Replication strategy should have the same number of endpoints for the base and the view"; + assert baseReplicas.size() == viewReplicas.size() : + String.format("Replication strategy should have the same number of endpoints for the base (%d) and the view (%d)", + baseReplicas.size(), viewReplicas.size()); int baseIdx = -1; for (int i=0; i(cm.tokenMap.tokens(peer).stream().map((token) -> token.getToken().getTokenValue().toString()).collect(Collectors.toList()))); + //.column(TRUNCATED_AT, status(cm)); // todo? + + return result; + } + + private static String status(ClusterMetadata cm) + { + if (StorageService.instance.isDraining()) + return StorageService.Mode.DRAINING.toString(); + if (StorageService.instance.isDrained()) + return StorageService.Mode.DRAINED.toString(); + return cm.directory.peerState(getBroadcastAddressAndPort()).toString(); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/virtual/PeersTable.java b/src/java/org/apache/cassandra/db/virtual/PeersTable.java new file mode 100644 index 0000000000..5b011de604 --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/PeersTable.java @@ -0,0 +1,200 @@ +/* + * 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.db.virtual; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.SystemKeyspace.LEGACY_PEERS; +import static org.apache.cassandra.db.SystemKeyspace.PEERS_V2; +import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; + +public class PeersTable extends AbstractVirtualTable +{ + + public static String PEER = "peer"; + public static String PEER_PORT = "peer_port"; + public static String DATA_CENTER = "data_center"; + public static String HOST_ID = "host_id"; + public static String PREFERRED_IP = "preferred_ip"; + public static String PREFERRED_PORT = "preferred_port"; + public static String RACK = "rack"; + public static String RELEASE_VERSION = "release_version"; + public static String NATIVE_ADDRESS = "native_address"; + public static String NATIVE_PORT = "native_port"; + public static String SCHEMA_VERSION = "schema_version"; + public static String TOKENS = "tokens"; + public static String STATE = "state"; + + public PeersTable(String keyspace) + { + super(TableMetadata.builder(keyspace, "peers") + .comment("Peers") + .kind(TableMetadata.Kind.VIRTUAL) + .partitioner(new LocalPartitioner(InetAddressType.instance)) + .addPartitionKeyColumn(PEER, InetAddressType.instance) + .addClusteringColumn(PEER_PORT, Int32Type.instance) + .addRegularColumn(DATA_CENTER, UTF8Type.instance) + .addRegularColumn(RACK, UTF8Type.instance) + .addRegularColumn(HOST_ID, UUIDType.instance) + .addRegularColumn(PREFERRED_IP, InetAddressType.instance) + .addRegularColumn(PREFERRED_PORT, Int32Type.instance) + .addRegularColumn(NATIVE_ADDRESS, InetAddressType.instance) + .addRegularColumn(NATIVE_PORT, Int32Type.instance) + .addRegularColumn(RELEASE_VERSION, UTF8Type.instance) + .addRegularColumn(SCHEMA_VERSION, UUIDType.instance) + .addRegularColumn(STATE, UTF8Type.instance) + .addRegularColumn(TOKENS, SetType.getInstance(UTF8Type.instance, false)) + .build()); + } + + public DataSet data() + { + SimpleDataSet result = new SimpleDataSet(metadata()); + + ClusterMetadata metadata = ClusterMetadata.current(); + for (InetAddressAndPort addr : metadata.directory.allJoinedEndpoints()) + { + NodeId peer = metadata.directory.peerId(addr); + + NodeAddresses addresses = metadata.directory.getNodeAddresses(peer); + result.row(addr.getAddress(), addr.getPort()) + .column(DATA_CENTER, metadata.directory.location(peer).datacenter) + .column(RACK, metadata.directory.location(peer).rack) + .column(HOST_ID, peer.toUUID()) + .column(PREFERRED_IP, addresses.broadcastAddress.getAddress()) + .column(PREFERRED_PORT, addresses.broadcastAddress.getPort()) + .column(NATIVE_ADDRESS, addresses.nativeAddress.getAddress()) + .column(NATIVE_PORT, addresses.nativeAddress.getPort()) + .column(RELEASE_VERSION, metadata.directory.version(peer).cassandraVersion.toString()) + .column(SCHEMA_VERSION, Schema.instance.getVersion()) //TODO + .column(STATE, metadata.directory.peerState(peer).toString()) + .column(TOKENS, new HashSet<>(metadata.tokenMap.tokens(peer).stream().map((token) -> token.getToken().getTokenValue().toString()).collect(Collectors.toList()))); + } + + return result; + } + + public static void initializeLegacyPeerTables(ClusterMetadata prev, ClusterMetadata next) + { + QueryProcessor.executeInternal(String.format("TRUNCATE %s.%s", SYSTEM_KEYSPACE_NAME, PEERS_V2)); + QueryProcessor.executeInternal(String.format("TRUNCATE %s.%s", SYSTEM_KEYSPACE_NAME, LEGACY_PEERS)); + + for (NodeId nodeId : next.directory.peerIds()) + updateLegacyPeerTable(nodeId, prev, next); + } + + private static String peers_v2_query = "INSERT INTO %s.%s (" + + "peer, peer_port, " + + "preferred_ip, preferred_port, " + + "native_address, native_port, " + + "data_center, rack, " + + "host_id, " + + "release_version, " + + "schema_version," + + "tokens) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?,?,?,?)"; + + private static String legacy_peers_query = "INSERT INTO %s.%s (" + + "peer, preferred_ip, rpc_address, " + + "data_center, rack, " + + "host_id, " + + "release_version, " + + "schema_version," + + "tokens) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"; + + private static String peers_delete_query = "DELETE FROM %s.%s WHERE peer=? and peer_port=?"; + private static String legacy_peers_delete_query = "DELETE FROM %s.%s WHERE peer=?"; + + private static final Logger logger = LoggerFactory.getLogger(PeersTable.class); + public static void updateLegacyPeerTable(NodeId nodeId, ClusterMetadata prev, ClusterMetadata next) + { + if (nodeId.equals(next.directory.peerId(FBUtilities.getBroadcastAddressAndPort()))) + return; + + if (next.directory.peerState(nodeId) == null || next.directory.peerState(nodeId) == NodeState.LEFT) + { + NodeAddresses addresses = prev.directory.getNodeAddresses(nodeId); + logger.debug("Purging {} from system.peers_v2 table", addresses); + QueryProcessor.executeInternal(String.format(peers_delete_query, SYSTEM_KEYSPACE_NAME, PEERS_V2), addresses.broadcastAddress.getAddress(), addresses.broadcastAddress.getPort()); + QueryProcessor.executeInternal(String.format(legacy_peers_delete_query, SYSTEM_KEYSPACE_NAME, LEGACY_PEERS), addresses.broadcastAddress.getAddress()); + } + else if (NodeState.isPreJoin(next.directory.peerState(nodeId))) + { + logger.debug("{} is in pre-join state {}, not updating system.peers_v2 table", nodeId, next.directory.peerState(nodeId)); + } + else + { + NodeAddresses addresses = next.directory.getNodeAddresses(nodeId); + NodeAddresses oldAddresses = prev.directory.getNodeAddresses(nodeId); + if (oldAddresses != null && !oldAddresses.equals(addresses)) + { + logger.debug("Purging {} from system.peers_v2 table", oldAddresses); + QueryProcessor.executeInternal(String.format(peers_delete_query, SYSTEM_KEYSPACE_NAME, PEERS_V2), oldAddresses.broadcastAddress.getAddress(), oldAddresses.broadcastAddress.getPort()); + QueryProcessor.executeInternal(String.format(legacy_peers_delete_query, SYSTEM_KEYSPACE_NAME, LEGACY_PEERS), oldAddresses.broadcastAddress.getAddress()); + } + + Location location = next.directory.location(nodeId); + + Set tokens = SystemKeyspace.tokensAsSet(next.tokenMap.tokens(nodeId)); + QueryProcessor.executeInternal(String.format(peers_v2_query, SYSTEM_KEYSPACE_NAME, PEERS_V2), + addresses.broadcastAddress.getAddress(), addresses.broadcastAddress.getPort(), + addresses.broadcastAddress.getAddress(), addresses.broadcastAddress.getPort(), + addresses.nativeAddress.getAddress(), addresses.nativeAddress.getPort(), + location.datacenter, location.rack, + nodeId.toUUID(), + next.directory.version(nodeId).cassandraVersion.toString(), + next.schema.getVersion(), + tokens); + + QueryProcessor.executeInternal(String.format(legacy_peers_query, SYSTEM_KEYSPACE_NAME, LEGACY_PEERS), + addresses.broadcastAddress.getAddress(), addresses.broadcastAddress.getAddress(), addresses.nativeAddress.getAddress(), + location.datacenter, location.rack, + nodeId.toUUID(), + next.directory.version(nodeId).cassandraVersion.toString(), + next.schema.getVersion(), + tokens); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java index 7d6152bdc2..7b7e28e30a 100644 --- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java +++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java @@ -52,6 +52,9 @@ public final class SystemViewsKeyspace extends VirtualKeyspace .add(new QueriesTable(VIRTUAL_VIEWS)) .add(new LogMessagesTable(VIRTUAL_VIEWS)) .add(new SnapshotsTable(VIRTUAL_VIEWS)) + .add(new PeersTable(VIRTUAL_VIEWS)) + .add(new LocalTable(VIRTUAL_VIEWS)) + .add(new ClusterMetadataLogTable(VIRTUAL_VIEWS)) .addAll(LocalRepairTables.getAll(VIRTUAL_VIEWS)) .addAll(CIDRFilteringMetricsTable.getAll(VIRTUAL_VIEWS)) .addAll(StorageAttachedIndexTables.getAll(VIRTUAL_VIEWS)) diff --git a/src/java/org/apache/cassandra/dht/AbstractBounds.java b/src/java/org/apache/cassandra/dht/AbstractBounds.java index 7a603b0a5d..9faee77d20 100644 --- a/src/java/org/apache/cassandra/dht/AbstractBounds.java +++ b/src/java/org/apache/cassandra/dht/AbstractBounds.java @@ -50,7 +50,7 @@ public abstract class AbstractBounds> implements Seria public AbstractBounds(T left, T right) { - assert left.getPartitioner() == right.getPartitioner(); + assert left.getPartitioner().getClass().equals(right.getPartitioner().getClass()); // todo: is this enough? this.left = left; this.right = right; } diff --git a/src/java/org/apache/cassandra/dht/BootStrapper.java b/src/java/org/apache/cassandra/dht/BootStrapper.java index 5d5529e15f..82f587ed30 100644 --- a/src/java/org/apache/cassandra/dht/BootStrapper.java +++ b/src/java/org/apache/cassandra/dht/BootStrapper.java @@ -17,25 +17,33 @@ */ package org.apache.cassandra.dht; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.cassandra.tcm.ownership.MovementMap; import org.apache.cassandra.utils.concurrent.Future; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.tokenallocator.TokenAllocation; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.streaming.*; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.streaming.StreamEvent; +import org.apache.cassandra.streaming.StreamEventHandler; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.streaming.StreamResultFuture; +import org.apache.cassandra.streaming.StreamState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.progress.ProgressEvent; import org.apache.cassandra.utils.progress.ProgressEventNotifierSupport; import org.apache.cassandra.utils.progress.ProgressEventType; @@ -47,39 +55,49 @@ public class BootStrapper extends ProgressEventNotifierSupport /* endpoint that needs to be bootstrapped */ protected final InetAddressAndPort address; /* token of the node being bootstrapped. */ - protected final Collection tokens; - protected final TokenMetadata tokenMetadata; + protected final ClusterMetadata metadata; + private final MovementMap movements; + private final MovementMap strictMovements; - public BootStrapper(InetAddressAndPort address, Collection tokens, TokenMetadata tmd) + public BootStrapper(InetAddressAndPort address, + ClusterMetadata metadata, + MovementMap movements, + MovementMap strictMovements) { assert address != null; - assert tokens != null && !tokens.isEmpty(); this.address = address; - this.tokens = tokens; - this.tokenMetadata = tmd; + this.metadata = metadata; + this.movements = movements; + this.strictMovements = strictMovements; } - public Future bootstrap(StreamStateStore stateStore, boolean useStrictConsistency) + public Future bootstrap(StreamStateStore stateStore, boolean useStrictConsistency, InetAddressAndPort beingReplaced) { logger.trace("Beginning bootstrap process"); - RangeStreamer streamer = new RangeStreamer(tokenMetadata, - tokens, - address, + RangeStreamer streamer = new RangeStreamer(metadata, StreamOperation.BOOTSTRAP, useStrictConsistency, DatabaseDescriptor.getEndpointSnitch(), stateStore, true, - DatabaseDescriptor.getStreamingConnectionsPerHost()); - final Collection nonLocalStrategyKeyspaces = Schema.instance.distributedKeyspaces().names(); + DatabaseDescriptor.getStreamingConnectionsPerHost(), + movements, + strictMovements); + + if (beingReplaced != null) + streamer.addSourceFilter(new RangeStreamer.ExcludedSourcesFilter(Collections.singleton(beingReplaced))); + + final Collection nonLocalStrategyKeyspaces = Schema.instance.getNonLocalStrategyKeyspaces().names(); if (nonLocalStrategyKeyspaces.isEmpty()) logger.debug("Schema does not contain any non-local keyspaces to stream on bootstrap"); for (String keyspaceName : nonLocalStrategyKeyspaces) { - AbstractReplicationStrategy strategy = Keyspace.open(keyspaceName).getReplicationStrategy(); - streamer.addRanges(keyspaceName, strategy.getPendingAddressRanges(tokenMetadata, tokens, address)); + KeyspaceMetadata ksm = metadata.schema.getKeyspaces().get(keyspaceName).get(); + if (ksm.params.replication.isMeta()) + continue; + streamer.addKeyspaceToFetch(keyspaceName); } StreamResultFuture bootstrapStreamResult = streamer.fetchAsync(); @@ -153,7 +171,7 @@ public class BootStrapper extends ProgressEventNotifierSupport * otherwise, if allocationKeyspace is specified use the token allocation algorithm to generate suitable tokens * else choose num_tokens tokens at random */ - public static Collection getBootstrapTokens(final TokenMetadata metadata, InetAddressAndPort address, long schemaTimeoutMillis, long ringTimeoutMillis) throws ConfigurationException + public static Collection getBootstrapTokens(final ClusterMetadata metadata, InetAddressAndPort address) throws ConfigurationException { String allocationKeyspace = DatabaseDescriptor.getAllocateTokensForKeyspace(); Integer allocationLocalRf = DatabaseDescriptor.getAllocateTokensForLocalRf(); @@ -174,10 +192,10 @@ public class BootStrapper extends ProgressEventNotifierSupport throw new ConfigurationException("num_tokens must be >= 1"); if (allocationKeyspace != null) - return allocateTokens(metadata, address, allocationKeyspace, numTokens, schemaTimeoutMillis, ringTimeoutMillis); + return allocateTokens(metadata, address, allocationKeyspace, numTokens); if (allocationLocalRf != null) - return allocateTokens(metadata, address, allocationLocalRf, numTokens, schemaTimeoutMillis, ringTimeoutMillis); + return allocateTokens(metadata, address, allocationLocalRf, numTokens); if (numTokens == 1) logger.warn("Picking random token for a single vnode. You should probably add more vnodes and/or use the automatic token allocation mechanism."); @@ -187,32 +205,26 @@ public class BootStrapper extends ProgressEventNotifierSupport return tokens; } - private static Collection getSpecifiedTokens(final TokenMetadata metadata, + private static Collection getSpecifiedTokens(final ClusterMetadata metadata, Collection initialTokens) { logger.info("tokens manually specified as {}", initialTokens); List tokens = new ArrayList<>(initialTokens.size()); for (String tokenString : initialTokens) { - Token token = metadata.partitioner.getTokenFactory().fromString(tokenString); - if (metadata.getEndpoint(token) != null) + Token token = metadata.tokenMap.partitioner().getTokenFactory().fromString(tokenString); + if (metadata.tokenMap.owner(token) != null) throw new ConfigurationException("Bootstrapping to existing token " + tokenString + " is not allowed (decommission/removenode the old node first)."); tokens.add(token); } return tokens; } - static Collection allocateTokens(final TokenMetadata metadata, + static Collection allocateTokens(final ClusterMetadata metadata, InetAddressAndPort address, String allocationKeyspace, - int numTokens, - long schemaTimeoutMillis, - long ringTimeoutMillis) + int numTokens) { - StorageService.instance.waitForSchema(schemaTimeoutMillis, ringTimeoutMillis); - if (!FBUtilities.getBroadcastAddressAndPort().equals(InetAddressAndPort.getLoopbackAddress())) - Gossiper.waitToSettle(); - Keyspace ks = Keyspace.open(allocationKeyspace); if (ks == null) throw new ConfigurationException("Problem opening token allocation keyspace " + allocationKeyspace); @@ -224,33 +236,35 @@ public class BootStrapper extends ProgressEventNotifierSupport } - static Collection allocateTokens(final TokenMetadata metadata, + static Collection allocateTokens(final ClusterMetadata metadata, InetAddressAndPort address, int rf, - int numTokens, - long schemaTimeoutMillis, - long ringTimeoutMillis) + int numTokens) { - StorageService.instance.waitForSchema(schemaTimeoutMillis, ringTimeoutMillis); - if (!FBUtilities.getBroadcastAddressAndPort().equals(InetAddressAndPort.getLoopbackAddress())) - Gossiper.waitToSettle(); - Collection tokens = TokenAllocation.allocateTokens(metadata, rf, address, numTokens); BootstrapDiagnostics.tokensAllocated(address, metadata, rf, numTokens, tokens); return tokens; } - public static Collection getRandomTokens(TokenMetadata metadata, int numTokens) + public static Set getRandomTokens(ClusterMetadata metadata, int numTokens) { Set tokens = new HashSet<>(numTokens); while (tokens.size() < numTokens) { - Token token = metadata.partitioner.getRandomToken(); - if (metadata.getEndpoint(token) == null) + Token token = metadata.tokenMap.partitioner().getRandomToken(); + if (metadata.tokenMap.owner(token) == null) tokens.add(token); } logger.info("Generated random tokens. tokens are {}", tokens); return tokens; } + + public String toString() + { + return "BootStrapper{" + + "address=" + address + + ", metadata=" + metadata + + '}'; + } } diff --git a/src/java/org/apache/cassandra/dht/BootstrapDiagnostics.java b/src/java/org/apache/cassandra/dht/BootstrapDiagnostics.java index 5c2b46a030..3f3bfbe2e4 100644 --- a/src/java/org/apache/cassandra/dht/BootstrapDiagnostics.java +++ b/src/java/org/apache/cassandra/dht/BootstrapDiagnostics.java @@ -19,12 +19,13 @@ package org.apache.cassandra.dht; import java.util.Collection; + import com.google.common.collect.ImmutableList; import org.apache.cassandra.dht.BootstrapEvent.BootstrapEventType; import org.apache.cassandra.diag.DiagnosticEventService; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; /** * Utility methods for bootstrap related activities. @@ -50,38 +51,38 @@ final class BootstrapDiagnostics ImmutableList.copyOf(initialTokens))); } - static void useRandomTokens(InetAddressAndPort address, TokenMetadata metadata, int numTokens, Collection tokens) + static void useRandomTokens(InetAddressAndPort address, ClusterMetadata metadata, int numTokens, Collection tokens) { if (isEnabled(BootstrapEventType.BOOTSTRAP_USING_RANDOM_TOKENS)) service.publish(new BootstrapEvent(BootstrapEventType.BOOTSTRAP_USING_RANDOM_TOKENS, address, - metadata.cloneOnlyTokenMap(), + metadata, null, null, numTokens, ImmutableList.copyOf(tokens))); } - static void tokensAllocated(InetAddressAndPort address, TokenMetadata metadata, + static void tokensAllocated(InetAddressAndPort address, ClusterMetadata metadata, String allocationKeyspace, int numTokens, Collection tokens) { if (isEnabled(BootstrapEventType.TOKENS_ALLOCATED)) service.publish(new BootstrapEvent(BootstrapEventType.TOKENS_ALLOCATED, address, - metadata.cloneOnlyTokenMap(), + metadata, allocationKeyspace, null, numTokens, ImmutableList.copyOf(tokens))); } - static void tokensAllocated(InetAddressAndPort address, TokenMetadata metadata, + static void tokensAllocated(InetAddressAndPort address, ClusterMetadata metadata, int rf, int numTokens, Collection tokens) { if (isEnabled(BootstrapEventType.TOKENS_ALLOCATED)) service.publish(new BootstrapEvent(BootstrapEventType.TOKENS_ALLOCATED, address, - metadata.cloneOnlyTokenMap(), + metadata, null, rf, numTokens, diff --git a/src/java/org/apache/cassandra/dht/BootstrapEvent.java b/src/java/org/apache/cassandra/dht/BootstrapEvent.java index 4936c2942a..e5a5cbc078 100644 --- a/src/java/org/apache/cassandra/dht/BootstrapEvent.java +++ b/src/java/org/apache/cassandra/dht/BootstrapEvent.java @@ -28,7 +28,7 @@ import com.google.common.collect.ImmutableCollection; import org.apache.cassandra.diag.DiagnosticEvent; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; /** * DiagnosticEvent implementation for bootstrap related activities. @@ -38,7 +38,7 @@ final class BootstrapEvent extends DiagnosticEvent private final BootstrapEventType type; @Nullable - private final TokenMetadata tokenMetadata; + private final ClusterMetadata metadata; private final InetAddressAndPort address; @Nullable private final String allocationKeyspace; @@ -47,12 +47,12 @@ final class BootstrapEvent extends DiagnosticEvent private final Integer numTokens; private final Collection tokens; - BootstrapEvent(BootstrapEventType type, InetAddressAndPort address, @Nullable TokenMetadata tokenMetadata, + BootstrapEvent(BootstrapEventType type, InetAddressAndPort address, @Nullable ClusterMetadata metadata, @Nullable String allocationKeyspace, @Nullable Integer rf, int numTokens, ImmutableCollection tokens) { this.type = type; this.address = address; - this.tokenMetadata = tokenMetadata; + this.metadata = metadata; this.allocationKeyspace = allocationKeyspace; this.rf = rf; this.numTokens = numTokens; @@ -76,7 +76,7 @@ final class BootstrapEvent extends DiagnosticEvent { // be extra defensive against nulls and bugs HashMap ret = new HashMap<>(); - ret.put("tokenMetadata", String.valueOf(tokenMetadata)); + ret.put("metadata", metadata.legacyToString()); ret.put("allocationKeyspace", allocationKeyspace); ret.put("rf", rf); ret.put("numTokens", numTokens); diff --git a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java index 98e4017342..4a6aa8d5a8 100644 --- a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java +++ b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java @@ -62,7 +62,7 @@ abstract class ComparableObjectToken> extends Token public int compareTo(Token o) { if (o.getClass() != getClass()) - throw new IllegalArgumentException("Invalid type of Token.compareTo() argument."); + throw new IllegalArgumentException(String.format("Invalid type of Token.compareTo() argument. %s != %s", o.getClass(), getClass())); return token.compareTo(((ComparableObjectToken) o).token); } diff --git a/src/java/org/apache/cassandra/dht/Datacenters.java b/src/java/org/apache/cassandra/dht/Datacenters.java index b1d96eb729..2c0328e8d3 100644 --- a/src/java/org/apache/cassandra/dht/Datacenters.java +++ b/src/java/org/apache/cassandra/dht/Datacenters.java @@ -22,13 +22,10 @@ import java.util.HashSet; import java.util.Set; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.locator.IEndpointSnitch; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; public class Datacenters { - private static class DCHandle { private static final String thisDc = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); @@ -41,22 +38,15 @@ public class Datacenters /* * (non-javadoc) Method to generate list of valid data center names to be used to validate the replication parameters during CREATE / ALTER keyspace operations. - * All peers of current node are fetched from {@link TokenMetadata} and then a set is build by fetching DC name of each peer. * @return a set of valid DC names */ - public static Set getValidDatacenters() + public static Set getValidDatacenters(ClusterMetadata metadata) { final Set validDataCenters = new HashSet<>(); - final IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - // Add data center of localhost. validDataCenters.add(thisDatacenter()); // Fetch and add DCs of all peers. - for (InetAddressAndPort peer : StorageService.instance.getTokenMetadata().getAllEndpoints()) - { - validDataCenters.add(snitch.getDatacenter(peer)); - } - + validDataCenters.addAll(metadata.directory.knownDatacenters()); return validDataCenters; } } diff --git a/src/java/org/apache/cassandra/dht/IPartitioner.java b/src/java/org/apache/cassandra/dht/IPartitioner.java index b1fcf8fed5..7e63cb422e 100644 --- a/src/java/org/apache/cassandra/dht/IPartitioner.java +++ b/src/java/org/apache/cassandra/dht/IPartitioner.java @@ -24,9 +24,9 @@ import java.util.Map; import java.util.Optional; import java.util.Random; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.service.StorageService; import javax.annotation.Nullable; @@ -34,7 +34,7 @@ public interface IPartitioner { static IPartitioner global() { - return StorageService.instance.getTokenMetadata().partitioner; + return DatabaseDescriptor.getPartitioner(); } static void validate(Collection> allBounds) diff --git a/src/java/org/apache/cassandra/dht/LocalPartitioner.java b/src/java/org/apache/cassandra/dht/LocalPartitioner.java index 74a1264c8d..185871d9a2 100644 --- a/src/java/org/apache/cassandra/dht/LocalPartitioner.java +++ b/src/java/org/apache/cassandra/dht/LocalPartitioner.java @@ -161,7 +161,9 @@ public class LocalPartitioner implements IPartitioner @Override public int compareTo(Token o) { - assert getPartitioner() == o.getPartitioner() : String.format("partitioners do not match; %s != %s", getPartitioner(), o.getPartitioner()); + // todo (tcm); seems partitioner got mutated on alter type (for example) before tcm, now we create a new one - not sure its enough just making sure that its the same type of partitioner + assert o.getPartitioner().getClass().equals(getPartitioner().getClass()); +// assert getPartitioner() == o.getPartitioner() : String.format("partitioners do not match; %s != %s", getPartitioner(), o.getPartitioner()); return comparator.compare(token, ((LocalToken) o).token); } diff --git a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java index 2566cbf710..304c20f5f9 100644 --- a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java +++ b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java @@ -45,6 +45,15 @@ public class OrderPreservingPartitioner implements IPartitioner private static final String rndchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; public static final StringToken MINIMUM = new StringToken(""); + public static final StringToken MAXIMUM = new StringToken("") { + public int compareTo(Token o) + { + if (o == MAXIMUM) + return 0; + + return 1; + } + }; public static final BigInteger CHAR_MASK = new BigInteger("65535"); @@ -116,6 +125,11 @@ public class OrderPreservingPartitioner implements IPartitioner return MINIMUM; } + public StringToken getMaximumToken() + { + return MAXIMUM; + } + public StringToken getRandomToken() { return getRandomToken(ThreadLocalRandom.current()); @@ -208,6 +222,16 @@ public class OrderPreservingPartitioner implements IPartitioner { return ByteSource.of(token, version); } + + @Override + public int compareTo(Token o) + { + // todo (rebase): I have no recollection of why this is needed - investigate + if (o == MAXIMUM) + return -1; + + return super.compareTo(o); + } } public StringToken getToken(ByteBuffer key) diff --git a/src/java/org/apache/cassandra/dht/OwnedRanges.java b/src/java/org/apache/cassandra/dht/OwnedRanges.java new file mode 100644 index 0000000000..dd60283d94 --- /dev/null +++ b/src/java/org/apache/cassandra/dht/OwnedRanges.java @@ -0,0 +1,139 @@ +/* + * 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.dht; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.StorageMetrics; + +public final class OwnedRanges +{ + private static final Logger logger = LoggerFactory.getLogger(OwnedRanges.class); + + private static final Comparator> rangeComparator = (r1, r2) -> + { + int cmp = r1.left.compareTo(r2.left); + + return cmp == 0 ? r1.right.compareTo(r2.right) : cmp; + }; + + // the set of token ranges that this node is a replica for + private final List> ownedRanges; + + public OwnedRanges(Collection> ownedRanges) + { + this.ownedRanges = Range.normalize(ownedRanges); + } + + /** + * Check that all ranges in a requested set are contained by those in the owned set. Used in several contexts, such + * as validating StreamRequests in StreamSession & PrepareMessage and ValidationRequest in RepairMessageVerbHandler. + * In those callers, we want to verify that the token ranges specified in some request from a peer are not outside + * the ranges owned by the local node. There are 2 levels of response if invalid ranges are detected, controlled + * by options in Config; logging the event and rejecting the request and either/neither/both of these options may be + * enabled. If neither are enabled, we short ciruit and immediately return success without any further processing. + * If either option is enabled and we do detect unowned ranges in the request, we increment a metric then take further + * action depending on the config. + * + * @param requestedRanges the set of token ranges contained in a request from a peer + * @param requestId an identifier for the peer request, to be used in logging (e.g. Stream or Repair Session #) + * @param requestType description of the request type, to be used in logging (e.g. "prepare request" or "validation") + * @param from the originator of the request + * @return true if the request should be accepted (either because no checking was performed, invalid ranges were d + * identified but only the logging action is enabled, or because all request ranges were valid. Otherwise, + * returns false to indicate the request should be rejected. + */ + public boolean validateRangeRequest(Collection> requestedRanges, String requestId, String requestType, InetAddressAndPort from) + { + Collection> unownedRanges = testRanges(requestedRanges); + + if (!unownedRanges.isEmpty()) + { + StorageMetrics.totalOpsForInvalidToken.inc(); + logger.warn("[{}] Received {} from {} containing ranges {} outside valid ranges {}", + requestId, + requestType, + from, + unownedRanges, + ownedRanges); + return false; + } + return true; + } + + /** + * Takes a collection of ranges and returns ranges from that collection that are not covered by the this node's owned ranges. + * + * This normalizes the range collections internally, so: + * a) be cautious about using this in any hot path + * b) any returned ranges may not be identical to those present. That is, the returned values are post-normalization. + * + * e.g Given two collections: + * { (0, 100], (100, 200] } + * { (90, 100], (100, 110], (110, 300] } + * the normalized forms are: + * { (0, 200] } + * { (90, 300] } + * and so the return value would be: + * { (90, 300] } + * which is equivalent, but not strictly equal to any member of the original supplied collection. + * + * @param testedRanges collection of candidate ranges to be checked + * @return the ranges in testedRanges which are not covered by the owned ranges + */ + @VisibleForTesting + Collection> testRanges(final Collection> testedRanges) + { + if (ownedRanges.isEmpty()) + return testedRanges; + + // now normalize the second and check coverage of its members in the normalized first collection + return Range.normalize(testedRanges).stream().filter(requested -> + { + // Find the point at which the target range would insert into the superset + int index = Collections.binarySearch(ownedRanges, requested, rangeComparator); + + // an index >= 0 means an exact match was found so we can definitely accept this range + if (index >= 0) + return false; + + // convert to an insertion point in the superset + index = Math.abs(index) - 1; + + // target sorts before the last list item, so we only need to check that one + if (index >= ownedRanges.size()) + return !ownedRanges.get(index - 1).contains(requested); + + // target sorts before the first list item, so we only need to check that one + if (index == 0) + return !ownedRanges.get(index).contains(requested); + + // otherwise, check if the range on either side of the insertion point wholly contains the target + return !(ownedRanges.get(index - 1).contains(requested) || ownedRanges.get(index).contains(requested)); + }).collect(Collectors.toSet()); + } +} diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java index 0ba6d20870..b5d06967ac 100644 --- a/src/java/org/apache/cassandra/dht/Range.java +++ b/src/java/org/apache/cassandra/dht/Range.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.dht; +import java.io.IOException; import java.io.Serializable; import java.util.*; import java.util.function.Predicate; @@ -25,6 +26,12 @@ import com.google.common.collect.Iterables; import org.apache.commons.lang3.ObjectUtils; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.utils.Pair; /** @@ -38,6 +45,7 @@ import org.apache.cassandra.utils.Pair; */ public class Range> extends AbstractBounds implements Comparable>, Serializable { + public static final Serializer serializer = new Serializer(); public static final long serialVersionUID = 1L; public Range(T left, T right) @@ -682,4 +690,24 @@ public class Range> extends AbstractBounds implemen } } } + + public static class Serializer implements MetadataSerializer> + { + private static final int SERDE_VERSION = MessagingService.VERSION_40; + + public void serialize(Range t, DataOutputPlus out, Version version) throws IOException + { + tokenSerializer.serialize(t, out, SERDE_VERSION); + } + + public Range deserialize(DataInputPlus in, Version version) throws IOException + { + return (Range) tokenSerializer.deserialize(in, ClusterMetadata.current().partitioner, SERDE_VERSION); + } + + public long serializedSize(Range t, Version version) + { + return tokenSerializer.serializedSize(t, SERDE_VERSION); + } + } } diff --git a/src/java/org/apache/cassandra/dht/RangeStreamer.java b/src/java/org/apache/cassandra/dht/RangeStreamer.java index 9b7833b90a..7cf919b91f 100644 --- a/src/java/org/apache/cassandra/dht/RangeStreamer.java +++ b/src/java/org/apache/cassandra/dht/RangeStreamer.java @@ -20,9 +20,9 @@ package org.apache.cassandra.dht; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -33,11 +33,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; - +import com.google.common.collect.Multimaps; import org.apache.commons.lang3.StringUtils; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,7 +45,6 @@ import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.EndpointsByRange; import org.apache.cassandra.locator.EndpointsByReplica; import org.apache.cassandra.locator.EndpointsForRange; @@ -60,11 +57,13 @@ import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; import org.apache.cassandra.locator.Replicas; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.streaming.StreamOperation; import org.apache.cassandra.streaming.StreamPlan; import org.apache.cassandra.streaming.StreamResultFuture; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.MovementMap; import org.apache.cassandra.utils.FBUtilities; import static com.google.common.base.Predicates.and; @@ -82,17 +81,12 @@ public class RangeStreamer private static final Logger logger = LoggerFactory.getLogger(RangeStreamer.class); public static Predicate ALIVE_PREDICATE = replica -> - (!Gossiper.instance.isEnabled() || - (Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()) == null || - Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()).isAlive())) && - FailureDetector.instance.isAlive(replica.endpoint()); + (!Gossiper.instance.isEnabled() || + (Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()) == null || + Gossiper.instance.getEndpointStateForEndpoint(replica.endpoint()).isAlive())) && + FailureDetector.instance.isAlive(replica.endpoint()); - /* bootstrap tokens. can be null if replacing the node. */ - private final Collection tokens; - /* current token ring */ - private final TokenMetadata metadata; - /* address of this node */ - private final InetAddressAndPort address; + private final ClusterMetadata metadata; /* streaming description */ private final String description; private final Map> toFetch = new HashMap<>(); @@ -101,6 +95,8 @@ public class RangeStreamer private final boolean useStrictConsistency; private final IEndpointSnitch snitch; private final StreamStateStore stateStore; + private final MovementMap movements; + private final MovementMap strictMovements; public static class FetchReplica { @@ -272,40 +268,61 @@ public class RangeStreamer } } - public RangeStreamer(TokenMetadata metadata, - Collection tokens, - InetAddressAndPort address, + public static class ExcludedSourcesFilter implements SourceFilter + { + private final Set excludedSources; + + public ExcludedSourcesFilter(Set allowedSources) + { + this.excludedSources = allowedSources; + } + + public boolean apply(Replica replica) + { + return !excludedSources.contains(replica.endpoint()); + } + + @Override + public String message(Replica replica) + { + return "Filtered " + replica + " out because it was in the excluded set: " + excludedSources; + } + } + + public RangeStreamer(ClusterMetadata metadata, StreamOperation streamOperation, boolean useStrictConsistency, IEndpointSnitch snitch, StreamStateStore stateStore, boolean connectSequentially, - int connectionsPerHost) + int connectionsPerHost, + MovementMap movements, + MovementMap strictMovements) { - this(metadata, tokens, address, streamOperation, useStrictConsistency, snitch, stateStore, - FailureDetector.instance, connectSequentially, connectionsPerHost); + this(metadata, streamOperation, useStrictConsistency, snitch, stateStore, + FailureDetector.instance, connectSequentially, connectionsPerHost, movements, strictMovements); } - RangeStreamer(TokenMetadata metadata, - Collection tokens, - InetAddressAndPort address, + RangeStreamer(ClusterMetadata metadata, StreamOperation streamOperation, boolean useStrictConsistency, IEndpointSnitch snitch, StreamStateStore stateStore, IFailureDetector failureDetector, boolean connectSequentially, - int connectionsPerHost) + int connectionsPerHost, + MovementMap movements, + MovementMap strictMovements) { Preconditions.checkArgument(streamOperation == StreamOperation.BOOTSTRAP || streamOperation == StreamOperation.REBUILD, streamOperation); this.metadata = metadata; - this.tokens = tokens; - this.address = address; this.description = streamOperation.getDescription(); this.streamPlan = new StreamPlan(streamOperation, connectionsPerHost, connectSequentially, null, PreviewKind.NONE); this.useStrictConsistency = useStrictConsistency; this.snitch = snitch; this.stateStore = stateStore; + this.movements = movements; + this.strictMovements = strictMovements; streamPlan.listeners(this.stateStore); // We're _always_ filtering out a local node and down sources @@ -339,9 +356,8 @@ public class RangeStreamer * Add ranges to be streamed for given keyspace. * * @param keyspaceName keyspace name - * @param replicas ranges to be streamed */ - public void addRanges(String keyspaceName, ReplicaCollection replicas) + public void addKeyspaceToFetch(String keyspaceName) { Keyspace keyspace = Keyspace.open(keyspaceName); AbstractReplicationStrategy strat = keyspace.getReplicationStrategy(); @@ -351,8 +367,15 @@ public class RangeStreamer return; } - boolean useStrictSource = useStrictSourcesForRanges(strat); - EndpointsByReplica fetchMap = calculateRangesToFetchWithPreferredEndpoints(replicas, keyspace, useStrictSource); + boolean useStrictSource = useStrictSourcesForRanges(keyspace.getMetadata().params.replication, strat); + EndpointsByReplica fetchMap = calculateRangesToFetchWithPreferredEndpoints(snitch::sortedByProximity, + keyspace.getReplicationStrategy(), + useStrictConsistency, + metadata, + keyspace.getName(), + sourceFilters, + movements, + strictMovements); for (Map.Entry entry : fetchMap.flattenEntries()) logger.info("{}: range {} exists on {} for keyspace {}", description, entry.getKey(), entry.getValue(), keyspaceName); @@ -386,65 +409,50 @@ public class RangeStreamer * @param strat AbstractReplicationStrategy of keyspace to check * @return true when the node is bootstrapping, useStrictConsistency is true and # of nodes in the cluster is more than # of replica */ - private boolean useStrictSourcesForRanges(AbstractReplicationStrategy strat) + private boolean useStrictSourcesForRanges(ReplicationParams params, AbstractReplicationStrategy strat) { - boolean res = useStrictConsistency && tokens != null; - + return useStrictSourcesForRanges(params, strat, metadata, useStrictConsistency, movements, strictMovements); + } + + private static boolean useStrictSourcesForRanges(ReplicationParams params, + AbstractReplicationStrategy strat, + ClusterMetadata metadata, + boolean useStrictConsistency, + MovementMap movements, + MovementMap strictMovements) + { + boolean res = useStrictConsistency && strictMovements != null; + if (res) { + // First, just to be safe verify that every movement has a strict equivalent + if (!strictMovements.get(params).keySet().containsAll(movements.get(params).keySet())) + return false; + int nodes = 0; + // only include joined endpoints, exclude REGISTERED or LEFT + HashSet allOtherNodes = new HashSet<>(metadata.directory.allJoinedEndpoints()); + allOtherNodes.remove(FBUtilities.getBroadcastAddressAndPort()); if (strat instanceof NetworkTopologyStrategy) { - ImmutableMultimap dc2Nodes = metadata.getDC2AllEndpoints(snitch); + ImmutableMultimap dc2Nodes = Multimaps.index(allOtherNodes, (ep) -> metadata.directory.location(metadata.directory.peerId(ep)).datacenter); NetworkTopologyStrategy ntps = (NetworkTopologyStrategy) strat; for (String dc : dc2Nodes.keySet()) nodes += ntps.getReplicationFactor(dc).allReplicas > 0 ? dc2Nodes.get(dc).size() : 0; } else - nodes = metadata.getSizeOfAllEndpoints(); - - res = nodes > strat.getReplicationFactor().allReplicas; + nodes = allOtherNodes.size(); + + res = nodes >= strat.getReplicationFactor().allReplicas; } - + return res; } /** - * Wrapper method to assemble the arguments for invoking the implementation with RangeStreamer's parameters - */ - private EndpointsByReplica calculateRangesToFetchWithPreferredEndpoints(ReplicaCollection fetchRanges, Keyspace keyspace, boolean useStrictConsistency) - { - AbstractReplicationStrategy strat = keyspace.getReplicationStrategy(); - - TokenMetadata tmd = metadata.cloneOnlyTokenMap(); - - TokenMetadata tmdAfter = null; - - if (tokens != null) - { - // Pending ranges - tmdAfter = tmd.cloneOnlyTokenMap(); - tmdAfter.updateNormalTokens(tokens, address); - } - else if (useStrictConsistency) - { - throw new IllegalArgumentException("Can't ask for strict consistency and not supply tokens"); - } - - return calculateRangesToFetchWithPreferredEndpoints(snitch::sortedByProximity, - strat, - fetchRanges, - useStrictConsistency, - tmd, - tmdAfter, - keyspace.getName(), - sourceFilters); - - } - - /** + * * Get a map of all ranges and the source that will be cleaned up once this bootstrapped node is added for the given ranges. * For each range, the list should only contain a single source. This allows us to consistently migrate data without violating * consistency. @@ -452,27 +460,24 @@ public class RangeStreamer public static EndpointsByReplica calculateRangesToFetchWithPreferredEndpoints(BiFunction snitchGetSortedListByProximity, AbstractReplicationStrategy strat, - ReplicaCollection fetchRanges, boolean useStrictConsistency, - TokenMetadata tmdBefore, - TokenMetadata tmdAfter, + ClusterMetadata metadata, String keyspace, - Collection sourceFilters) + Collection sourceFilters, + MovementMap movements, + MovementMap strictMovements) { - EndpointsByRange rangeAddresses = strat.getRangeAddresses(tmdBefore); - InetAddressAndPort localAddress = FBUtilities.getBroadcastAddressAndPort(); - logger.debug ("Keyspace: {}", keyspace); - logger.debug("To fetch RN: {}", fetchRanges); - logger.debug("Fetch ranges: {}", rangeAddresses); + ReplicationParams params = metadata.schema.getKeyspaces().get(keyspace).get().params.replication; + logger.debug("Keyspace: {}", keyspace); + logger.debug("To fetch RN: {}", movements.get(params).keySet()); Predicate testSourceFilters = and(sourceFilters); - Function sorted = - endpoints -> snitchGetSortedListByProximity.apply(localAddress, endpoints); + Function sorted = endpoints -> snitchGetSortedListByProximity.apply(localAddress, endpoints); //This list of replicas is just candidates. With strict consistency it's going to be a narrow list. EndpointsByReplica.Builder rangesToFetchWithPreferredEndpoints = new EndpointsByReplica.Builder(); - for (Replica toFetch : fetchRanges) + for (Replica toFetch : movements.get(params).keySet()) { //Replica that is sufficient to provide the data we need //With strict consistency and transient replication we may end up with multiple types @@ -480,76 +485,55 @@ public class RangeStreamer Predicate isSufficient = r -> toFetch.isTransient() || r.isFull(); logger.debug("To fetch {}", toFetch); - for (Range range : rangeAddresses.keySet()) + + //Ultimately we populate this with whatever is going to be fetched from to satisfy toFetch + //It could be multiple endpoints and we must fetch from all of them if they are there + //With transient replication and strict consistency this is to get the full data from a full replica and + //transient data from the transient replica losing data + EndpointsForRange sources; + //Due to CASSANDRA-5953 we can have a higher RF than we have endpoints. + //So we need to be careful to only be strict when endpoints == RF + boolean isStrictConsistencyApplicable = useStrictConsistency && (movements.get(params).get(toFetch).size() == strat.getReplicationFactor().allReplicas); + if (isStrictConsistencyApplicable) { - if (!range.contains(toFetch.range())) - continue; + EndpointsForRange strictEndpoints = strictMovements.get(params).get(toFetch); - final EndpointsForRange oldEndpoints = sorted.apply(rangeAddresses.get(range)); + if (strictEndpoints.stream().filter(Replica::isFull).count() > 1) + throw new AssertionError("Expected <= 1 endpoint but found " + strictEndpoints); - //Ultimately we populate this with whatever is going to be fetched from to satisfy toFetch - //It could be multiple endpoints and we must fetch from all of them if they are there - //With transient replication and strict consistency this is to get the full data from a full replica and - //transient data from the transient replica losing data - EndpointsForRange sources; - //Due to CASSANDRA-5953 we can have a higher RF than we have endpoints. - //So we need to be careful to only be strict when endpoints == RF - boolean isStrictConsistencyApplicable = useStrictConsistency && (oldEndpoints.size() == strat.getReplicationFactor().allReplicas); - if (isStrictConsistencyApplicable) - { - EndpointsForRange strictEndpoints; + //We have to check the source filters here to see if they will remove any replicas + //required for strict consistency + if (!all(strictEndpoints, testSourceFilters)) + throw new IllegalStateException("Necessary replicas for strict consistency were removed by source filters: " + buildErrorMessage(sourceFilters, strictEndpoints)); - //Start with two sets of who replicates the range before and who replicates it after - EndpointsForRange newEndpoints = strat.calculateNaturalReplicas(toFetch.range().right, tmdAfter); - logger.debug("Old endpoints {}", oldEndpoints); - logger.debug("New endpoints {}", newEndpoints); + //If we are transitioning from transient to full and and the set of replicas for the range is not changing + //we might end up with no endpoints to fetch from by address. In that case we can pick any full replica safely + //since we are already a transient replica and the existing replica remains. + //The old behavior where we might be asked to fetch ranges we don't need shouldn't occur anymore. + //So it's an error if we don't find what we need. + if (strictEndpoints.isEmpty() && toFetch.isTransient()) + throw new AssertionError("If there are no endpoints to fetch from then we must be transitioning from transient to full for range " + toFetch); - // Remove new endpoints from old endpoints based on address - strictEndpoints = oldEndpoints.without(newEndpoints.endpoints()); + // we now add all potential strict endpoints when building the strictMovements, if we still have no full replicas for toFetch we should fail + if (!any(strictEndpoints, isSufficient)) + throw new IllegalStateException("Couldn't find any matching sufficient replica out of " + buildErrorMessage(sourceFilters, movements.get(params).get(toFetch))); - if (strictEndpoints.size() > 1) - throw new AssertionError("Expected <= 1 endpoint but found " + strictEndpoints); - - //We have to check the source filters here to see if they will remove any replicas - //required for strict consistency - if (!all(strictEndpoints, testSourceFilters)) - throw new IllegalStateException("Necessary replicas for strict consistency were removed by source filters: " + buildErrorMessage(sourceFilters, strictEndpoints)); - - //If we are transitioning from transient to full and and the set of replicas for the range is not changing - //we might end up with no endpoints to fetch from by address. In that case we can pick any full replica safely - //since we are already a transient replica and the existing replica remains. - //The old behavior where we might be asked to fetch ranges we don't need shouldn't occur anymore. - //So it's an error if we don't find what we need. - if (strictEndpoints.isEmpty() && toFetch.isTransient()) - throw new AssertionError("If there are no endpoints to fetch from then we must be transitioning from transient to full for range " + toFetch); - - if (!any(strictEndpoints, isSufficient)) - { - // need an additional replica; include all our filters, to ensure we include a matching node - Optional fullReplica = Iterables.tryFind(oldEndpoints, and(isSufficient, testSourceFilters)).toJavaUtil(); - if (fullReplica.isPresent()) - strictEndpoints = Endpoints.concat(strictEndpoints, EndpointsForRange.of(fullReplica.get())); - else - throw new IllegalStateException("Couldn't find any matching sufficient replica out of " + buildErrorMessage(sourceFilters, oldEndpoints)); - } - - sources = strictEndpoints; - } - else - { - //Without strict consistency we have given up on correctness so no point in fetching from - //a random full + transient replica since it's also likely to lose data - //Also apply testSourceFilters that were given to us so we can safely select a single source - sources = sorted.apply(oldEndpoints.filter(and(isSufficient, testSourceFilters))); - //Limit it to just the first possible source, we don't need more than one and downstream - //will fetch from every source we supply - sources = sources.size() > 0 ? sources.subList(0, 1) : sources; - } - - // storing range and preferred endpoint set - rangesToFetchWithPreferredEndpoints.putAll(toFetch, sources, Conflict.NONE); - logger.debug("Endpoints to fetch for {} are {}", toFetch, sources); + sources = strictEndpoints; } + else + { + //Without strict consistency we have given up on correctness so no point in fetching from + //a random full + transient replica since it's also likely to lose data + //Also apply testSourceFilters that were given to us so we can safely select a single source + sources = sorted.apply(movements.get(params).get(toFetch).filter(and(isSufficient, testSourceFilters))); + //Limit it to just the first possible source, we don't need more than one and downstream + //will fetch from every source we supply + sources = sources.size() > 0 ? sources.subList(0, 1) : sources; + } + + // storing range and preferred endpoint set + rangesToFetchWithPreferredEndpoints.putAll(toFetch, sources, Conflict.NONE); + logger.debug("Endpoints to fetch for {} are {}", toFetch, sources); EndpointsForRange addressList = rangesToFetchWithPreferredEndpoints.getIfPresent(toFetch); if (addressList == null) @@ -669,7 +653,7 @@ public class RangeStreamer if(entry.getKey().equals(FBUtilities.getBroadcastAddressAndPort())) { throw new IllegalStateException("Trying to stream locally. Range: " + entry.getValue() - + " in keyspace " + keyspace); + + " in keyspace " + keyspace); } if (!rangesWithSources.get(entry.getValue()).endpoints().contains(entry.getKey())) @@ -708,7 +692,7 @@ public class RangeStreamer else { // Filter out already streamed ranges - SystemKeyspace.AvailableRanges available = stateStore.getAvailableRanges(keyspace, metadata.partitioner); + SystemKeyspace.AvailableRanges available = stateStore.getAvailableRanges(keyspace, metadata.tokenMap.partitioner()); Predicate isAvailable = fetch -> { boolean isInFull = available.full.contains(fetch.local.range()); @@ -758,13 +742,13 @@ public class RangeStreamer InetAddressAndPort self = FBUtilities.getBroadcastAddressAndPort(); RangesAtEndpoint full = remaining.stream() - .filter(pair -> pair.remote.isFull()) - .map(pair -> pair.local) - .collect(RangesAtEndpoint.collector(self)); + .filter(pair -> pair.remote.isFull()) + .map(pair -> pair.local) + .collect(RangesAtEndpoint.collector(self)); RangesAtEndpoint transientReplicas = remaining.stream() - .filter(pair -> pair.remote.isTransient()) - .map(pair -> pair.local) - .collect(RangesAtEndpoint.collector(self)); + .filter(pair -> pair.remote.isTransient()) + .map(pair -> pair.local) + .collect(RangesAtEndpoint.collector(self)); logger.debug("Source and our replicas {}", fetchReplicas); logger.debug("Source {} Keyspace {} streaming full {} transient {}", source, keyspace, full, transientReplicas); diff --git a/src/java/org/apache/cassandra/dht/Token.java b/src/java/org/apache/cassandra/dht/Token.java index fda7f30751..41694277d8 100644 --- a/src/java/org/apache/cassandra/dht/Token.java +++ b/src/java/org/apache/cassandra/dht/Token.java @@ -25,15 +25,20 @@ import java.nio.ByteBuffer; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.tcm.serialization.PartitionerAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.net.MessagingService; public abstract class Token implements RingPosition, Serializable { private static final long serialVersionUID = 1L; public static final TokenSerializer serializer = new TokenSerializer(); + public static final MetadataSerializer metadataSerializer = new MetadataSerializer(); public static abstract class TokenFactory { @@ -90,6 +95,28 @@ public abstract class Token implements RingPosition, Serializable } } + public static class MetadataSerializer implements PartitionerAwareMetadataSerializer + { + private static final int SERDE_VERSION = MessagingService.VERSION_40; + + public void serialize(Token t, DataOutputPlus out, Version version) throws IOException + { + serializer.serialize(t, out, SERDE_VERSION); + } + + public Token deserialize(DataInputPlus in, IPartitioner partitioner, Version version) throws IOException + { + // This is only ever used to deserialize Tokens from this cluster and as the partitioner can + // never be changed, it's safe to assume that the right implementation is provided by ClusterMetadata + return serializer.deserialize(in, partitioner, SERDE_VERSION); + } + + public long serializedSize(Token t, Version version) + { + return serializer.serializedSize(t, SERDE_VERSION); + } + } + public static class TokenSerializer implements IPartitionerDependentSerializer { public void serialize(Token token, DataOutputPlus out, int version) throws IOException diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/OfflineTokenAllocator.java b/src/java/org/apache/cassandra/dht/tokenallocator/OfflineTokenAllocator.java index 6a382c0d51..d46c043ed5 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/OfflineTokenAllocator.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/OfflineTokenAllocator.java @@ -36,7 +36,7 @@ import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.SimpleSnitch; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.OutputHandler; public class OfflineTokenAllocator @@ -115,7 +115,6 @@ public class OfflineTokenAllocator private static class MultinodeAllocator { private final FakeSnitch fakeSnitch; - private final TokenMetadata fakeMetadata; private final TokenAllocation allocation; private final Map lastCheckPoint = Maps.newHashMap(); private final OutputHandler logger; @@ -123,8 +122,7 @@ public class OfflineTokenAllocator private MultinodeAllocator(int rf, int numTokens, OutputHandler logger, IPartitioner partitioner) { this.fakeSnitch = new FakeSnitch(); - this.fakeMetadata = new TokenMetadata(fakeSnitch).cloneWithNewPartitioner(partitioner); - this.allocation = TokenAllocation.create(fakeSnitch, fakeMetadata, rf, numTokens); + this.allocation = TokenAllocation.create(fakeSnitch, new ClusterMetadata(partitioner), rf, numTokens); this.logger = logger; } @@ -133,7 +131,8 @@ public class OfflineTokenAllocator // Update snitch and token metadata info InetAddressAndPort fakeNodeAddressAndPort = getLoopbackAddressWithPort(nodeId); fakeSnitch.nodeByRack.put(fakeNodeAddressAndPort, rackId); - fakeMetadata.updateTopology(fakeNodeAddressAndPort); + // todo: + //fakeMetadata.updateTopology(fakeNodeAddressAndPort); // Allocate tokens Collection tokens = allocation.allocate(fakeNodeAddressAndPort); diff --git a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java index 7e46b87855..bad7785ba0 100644 --- a/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java +++ b/src/java/org/apache/cassandra/dht/tokenallocator/TokenAllocation.java @@ -27,6 +27,7 @@ import java.util.TreeMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import org.apache.commons.math3.stat.descriptive.SummaryStatistics; @@ -41,56 +42,55 @@ import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.NetworkTopologyStrategy; import org.apache.cassandra.locator.SimpleStrategy; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.locator.TokenMetadata.Topology; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; public class TokenAllocation { public static final double WARN_STDEV_GROWTH = 0.05; private static final Logger logger = LoggerFactory.getLogger(TokenAllocation.class); - final TokenMetadata tokenMetadata; + final ClusterMetadata metadata; final AbstractReplicationStrategy replicationStrategy; final int numTokens; final Map> strategyByRackDc = new HashMap<>(); - private TokenAllocation(TokenMetadata tokenMetadata, AbstractReplicationStrategy replicationStrategy, int numTokens) + private TokenAllocation(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, int numTokens) { - this.tokenMetadata = tokenMetadata.cloneOnlyTokenMap(); + this.metadata = metadata; this.replicationStrategy = replicationStrategy; this.numTokens = numTokens; } - public static Collection allocateTokens(final TokenMetadata tokenMetadata, + public static Collection allocateTokens(final ClusterMetadata metadata, final AbstractReplicationStrategy rs, final InetAddressAndPort endpoint, int numTokens) { - return create(tokenMetadata, rs, numTokens).allocate(endpoint); + return create(metadata, rs, numTokens).allocate(endpoint); } - public static Collection allocateTokens(final TokenMetadata tokenMetadata, + public static Collection allocateTokens(final ClusterMetadata metadata, final int replicas, final InetAddressAndPort endpoint, int numTokens) { - return create(DatabaseDescriptor.getEndpointSnitch(), tokenMetadata, replicas, numTokens).allocate(endpoint); + return create(DatabaseDescriptor.getEndpointSnitch(), metadata, replicas, numTokens).allocate(endpoint); } - static TokenAllocation create(IEndpointSnitch snitch, TokenMetadata tokenMetadata, int replicas, int numTokens) + static TokenAllocation create(IEndpointSnitch snitch, ClusterMetadata metadata, int replicas, int numTokens) { // We create a fake NTS replication strategy with the specified RF in the local DC HashMap options = new HashMap<>(); options.put(snitch.getLocalDatacenter(), Integer.toString(replicas)); - NetworkTopologyStrategy fakeReplicationStrategy = new NetworkTopologyStrategy(null, tokenMetadata, snitch, options); + NetworkTopologyStrategy fakeReplicationStrategy = new NetworkTopologyStrategy(null, options); - TokenAllocation allocator = new TokenAllocation(tokenMetadata, fakeReplicationStrategy, numTokens); - return allocator; + return new TokenAllocation(metadata, fakeReplicationStrategy, numTokens); } - static TokenAllocation create(TokenMetadata tokenMetadata, AbstractReplicationStrategy rs, int numTokens) + static TokenAllocation create(ClusterMetadata metadata, AbstractReplicationStrategy rs, int numTokens) { - return new TokenAllocation(tokenMetadata, rs, numTokens); + return new TokenAllocation(metadata, rs, numTokens); } Collection allocate(InetAddressAndPort endpoint) @@ -100,7 +100,6 @@ public class TokenAllocation tokens = strategy.adjustForCrossDatacenterClashes(tokens); SummaryStatistics os = strategy.replicatedOwnershipStats(); - tokenMetadata.updateNormalTokens(tokens, endpoint); SummaryStatistics ns = strategy.replicatedOwnershipStats(); logger.info("Selected tokens {}", tokens); @@ -142,12 +141,14 @@ public class TokenAllocation final TokenAllocator createAllocator() { NavigableMap sortedTokens = new TreeMap<>(); - for (Map.Entry en : tokenMetadata.getNormalAndBootstrappingTokenToEndpointMap().entrySet()) + + for (Map.Entry en : metadata.tokenMap.asMap().entrySet()) { - if (inAllocationRing(en.getValue())) - sortedTokens.put(en.getKey(), en.getValue()); + InetAddressAndPort endpoint = metadata.directory.endpoint(en.getValue()); + if (inAllocationRing(endpoint)) + sortedTokens.put(en.getKey(), endpoint); } - return TokenAllocatorFactory.createTokenAllocator(sortedTokens, this, tokenMetadata.partitioner); + return TokenAllocatorFactory.createTokenAllocator(sortedTokens, this, metadata.tokenMap.partitioner()); } final Collection adjustForCrossDatacenterClashes(Collection tokens) @@ -156,9 +157,10 @@ public class TokenAllocation for (Token t : tokens) { - while (tokenMetadata.getEndpoint(t) != null) + while (metadata.tokenMap.owner(t) != null) { - InetAddressAndPort other = tokenMetadata.getEndpoint(t); + NodeId nodeId = metadata.tokenMap.owner(t); + InetAddressAndPort other = metadata.directory.endpoint(nodeId); if (inAllocationRing(other)) throw new ConfigurationException(String.format("Allocated token %s already assigned to node %s. Is another node also allocating tokens?", t, other)); t = t.nextValidToken(); @@ -175,7 +177,10 @@ public class TokenAllocation { // Filter only in the same allocation ring if (inAllocationRing(en.getKey())) - stat.addValue(en.getValue() / tokenMetadata.getTokens(en.getKey()).size()); + { + NodeId nodeId = metadata.directory.peerId(en.getKey()); + stat.addValue(en.getValue() / metadata.tokenMap.tokens(nodeId).size()); + } } return stat; } @@ -184,7 +189,7 @@ public class TokenAllocation private Map evaluateReplicatedOwnership() { Map ownership = Maps.newHashMap(); - List sortedTokens = tokenMetadata.sortedTokens(); + List sortedTokens = metadata.tokenMap.tokens(); if (sortedTokens.isEmpty()) return ownership; @@ -205,7 +210,7 @@ public class TokenAllocation { double size = current.size(next); Token representative = current.getPartitioner().midpoint(current, next); - for (InetAddressAndPort n : replicationStrategy.calculateNaturalReplicas(representative, tokenMetadata).endpoints()) + for (InetAddressAndPort n : replicationStrategy.calculateNaturalReplicas(representative, metadata).endpoints()) { Double v = ownership.get(n); ownership.put(n, v != null ? v + size : size); @@ -215,8 +220,9 @@ public class TokenAllocation private StrategyAdapter getOrCreateStrategy(InetAddressAndPort endpoint) { - String dc = replicationStrategy.snitch.getDatacenter(endpoint); - String rack = replicationStrategy.snitch.getRack(endpoint); + IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); + String dc = snitch.getDatacenter(endpoint); + String rack = snitch.getRack(endpoint); return getOrCreateStrategy(dc, rack); } @@ -228,7 +234,7 @@ public class TokenAllocation private StrategyAdapter createStrategy(String dc, String rack) { if (replicationStrategy instanceof NetworkTopologyStrategy) - return createStrategy(tokenMetadata, (NetworkTopologyStrategy) replicationStrategy, dc, rack); + return createStrategy(metadata, (NetworkTopologyStrategy) replicationStrategy, dc, rack); if (replicationStrategy instanceof SimpleStrategy) return createStrategy((SimpleStrategy) replicationStrategy); throw new ConfigurationException("Token allocation does not support replication strategy " + replicationStrategy.getClass().getSimpleName()); @@ -236,37 +242,37 @@ public class TokenAllocation private StrategyAdapter createStrategy(final SimpleStrategy rs) { - return createStrategy(rs.snitch, null, null, rs.getReplicationFactor().allReplicas, false); + return createStrategy(DatabaseDescriptor.getEndpointSnitch(), null, null, rs.getReplicationFactor().allReplicas, false); } - private StrategyAdapter createStrategy(TokenMetadata tokenMetadata, NetworkTopologyStrategy strategy, String dc, String rack) + private StrategyAdapter createStrategy(ClusterMetadata metadata, NetworkTopologyStrategy strategy, String dc, String rack) { int replicas = strategy.getReplicationFactor(dc).allReplicas; - Topology topology = tokenMetadata.getTopology(); // if topology hasn't been setup yet for this dc+rack then treat it as a separate unit - int racks = topology.getDatacenterRacks().get(dc) != null && topology.getDatacenterRacks().get(dc).containsKey(rack) - ? topology.getDatacenterRacks().get(dc).asMap().size() + Multimap datacenterRacks = metadata.directory.datacenterRacks(dc); + int racks = datacenterRacks != null && datacenterRacks.containsKey(rack) + ? datacenterRacks.asMap().size() : 1; if (replicas <= 1) { // each node is treated as separate and replicates once - return createStrategy(strategy.snitch, dc, null, 1, false); + return createStrategy(DatabaseDescriptor.getEndpointSnitch(), dc, null, 1, false); } else if (racks == replicas) { // each node is treated as separate and replicates once, with separate allocation rings for each rack - return createStrategy(strategy.snitch, dc, rack, 1, false); + return createStrategy(DatabaseDescriptor.getEndpointSnitch(), dc, rack, 1, false); } else if (racks > replicas) { // group by rack - return createStrategy(strategy.snitch, dc, null, replicas, true); + return createStrategy(DatabaseDescriptor.getEndpointSnitch(), dc, null, replicas, true); } else if (racks == 1) { - return createStrategy(strategy.snitch, dc, null, replicas, false); + return createStrategy(DatabaseDescriptor.getEndpointSnitch(), dc, null, replicas, false); } throw new ConfigurationException(String.format("Token allocation failed: the number of racks %d in datacenter %s is lower than its replication factor %d.", diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterSyncPendingRanges.java b/src/java/org/apache/cassandra/exceptions/CoordinatorBehindException.java similarity index 72% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterSyncPendingRanges.java rename to src/java/org/apache/cassandra/exceptions/CoordinatorBehindException.java index 04065ae030..3ca1b51e23 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterSyncPendingRanges.java +++ b/src/java/org/apache/cassandra/exceptions/CoordinatorBehindException.java @@ -16,14 +16,17 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.exceptions; -import org.apache.cassandra.simulator.Actions.ReliableAction; - -public class OnClusterSyncPendingRanges extends ReliableAction +public class CoordinatorBehindException extends RuntimeException { - public OnClusterSyncPendingRanges(ClusterActions actions) + public CoordinatorBehindException(String msg) { - super("Sync Pending Ranges Executor", actions::syncPendingRanges, true); + super(msg); + } + + public CoordinatorBehindException(String msg, UnknownTableException cause) + { + super(msg, cause); } } diff --git a/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java b/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java new file mode 100644 index 0000000000..8fa34d965c --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java @@ -0,0 +1,62 @@ +/* + * 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.exceptions; + +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; + +public class InvalidRoutingException extends InvalidRequestException +{ + public static final String TOKEN_TEMPLATE = "Received a read request from %s for a token %s that is not owned by the current replica as of %s: %s."; + public static final String RANGE_TEMPLATE = "Received a read request from %s for a range [%s,%s] that is not owned by the current replica as of %s: %s."; + + public static final String WRITE_TEMPLATE = "Received a mutation from %s for a token %s that is not owned by the current replica as of %s: %s."; + private InvalidRoutingException(String msg) + { + super(msg); + } + + public static InvalidRoutingException forTokenRead(InetAddressAndPort from, + Token token, + Epoch epoch, + ReadCommand command) + { + return new InvalidRoutingException(String.format(TOKEN_TEMPLATE, from, token, epoch, command)); + } + + public static InvalidRoutingException forRangeRead(InetAddressAndPort from, + AbstractBounds range, + Epoch epoch, + ReadCommand command) + { + return new InvalidRoutingException(String.format(RANGE_TEMPLATE, from, range.left, range.right, epoch, command)); + } + + public static InvalidRoutingException forWrite(InetAddressAndPort from, + Token token, + Epoch epoch, + IMutation mutation) + { + return new InvalidRoutingException(String.format(WRITE_TEMPLATE, from, token, epoch, mutation.getKeyspaceName())); + } +} diff --git a/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java index 1703c4e180..488e656ae2 100644 --- a/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java +++ b/src/java/org/apache/cassandra/exceptions/RequestFailureReason.java @@ -23,6 +23,7 @@ import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.NotCMSException; import org.apache.cassandra.utils.vint.VIntCoding; import static java.lang.Math.max; @@ -36,7 +37,12 @@ public enum RequestFailureReason INCOMPATIBLE_SCHEMA (3), READ_SIZE (4), NODE_DOWN (5), - INDEX_NOT_AVAILABLE (6); + INDEX_NOT_AVAILABLE (6), + NOT_CMS (7), + INVALID_ROUTING (8), + COORDINATOR_BEHIND (9), + ; + public static final Serializer serializer = new Serializer(); @@ -86,6 +92,15 @@ public enum RequestFailureReason if (t instanceof IncompatibleSchemaException) return INCOMPATIBLE_SCHEMA; + if (t instanceof NotCMSException) + return NOT_CMS; + + if (t instanceof InvalidRoutingException) + return INVALID_ROUTING; + + if (t instanceof CoordinatorBehindException) + return COORDINATOR_BEHIND; + return UNKNOWN; } diff --git a/src/java/org/apache/cassandra/exceptions/UnavailableException.java b/src/java/org/apache/cassandra/exceptions/UnavailableException.java index d6e8488e22..8454a68f9a 100644 --- a/src/java/org/apache/cassandra/exceptions/UnavailableException.java +++ b/src/java/org/apache/cassandra/exceptions/UnavailableException.java @@ -34,7 +34,8 @@ public class UnavailableException extends RequestExecutionException public static UnavailableException create(ConsistencyLevel consistency, int required, int requiredFull, int alive, int aliveFull) { if (required > alive) - return new UnavailableException("Cannot achieve consistency level " + consistency, consistency, required, alive); + return new UnavailableException(String.format("Cannot achieve consistency level %s. Required %s but only %s alive.", consistency, required, alive), + consistency, required, alive); assert requiredFull < aliveFull; return new UnavailableException("Insufficient full replicas", consistency, required, alive); } diff --git a/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java b/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java index 4b4ce3865c..33c96c951e 100644 --- a/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java @@ -26,7 +26,8 @@ public class WriteTimeoutException extends RequestTimeoutException public WriteTimeoutException(WriteType writeType, ConsistencyLevel consistency, int received, int blockFor) { - super(ExceptionCode.WRITE_TIMEOUT, consistency, received, blockFor); + super(ExceptionCode.WRITE_TIMEOUT, consistency, received, blockFor, + String.format("Operation timed out - received %d/%d responses.", received, blockFor)); this.writeType = writeType; } diff --git a/src/java/org/apache/cassandra/gms/ApplicationState.java b/src/java/org/apache/cassandra/gms/ApplicationState.java index 2782d483e6..f831ef51af 100644 --- a/src/java/org/apache/cassandra/gms/ApplicationState.java +++ b/src/java/org/apache/cassandra/gms/ApplicationState.java @@ -31,24 +31,24 @@ public enum ApplicationState /** @deprecated See CASSANDRA-7544 */ @Deprecated(since = "4.0") STATUS, //Deprecated and unsued in 4.0, stop publishing in 5.0, reclaim in 6.0 LOAD, - SCHEMA, - DC, - RACK, - RELEASE_VERSION, - REMOVAL_COORDINATOR, + @Deprecated(since = "CEP-21") SCHEMA, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") DC, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") RACK, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") RELEASE_VERSION, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") REMOVAL_COORDINATOR, /** @deprecated See CASSANDRA-7544 */ - @Deprecated(since = "4.0") INTERNAL_IP, //Deprecated and unused in 4.0, stop publishing in 5.0, reclaim in 6.0 + @Deprecated(since = "4.0") INTERNAL_IP, //derived from ClusterMetadata, Deprecated and unused in 4.0, stop publishing in 5.0, reclaim in 6.0 /** @deprecated See CASSANDRA-7544 */ @Deprecated(since = "4.0") RPC_ADDRESS, // ^ Same X_11_PADDING, // padding specifically for 1.1 SEVERITY, - NET_VERSION, - HOST_ID, - TOKENS, + NET_VERSION, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") HOST_ID, // derived from ClusterMetadata + @Deprecated(since = "CEP-21") TOKENS, // derived from ClusterMetadata RPC_READY, // pad to allow adding new states to existing cluster - INTERNAL_ADDRESS_AND_PORT, //Replacement for INTERNAL_IP with up to two ports - NATIVE_ADDRESS_AND_PORT, //Replacement for RPC_ADDRESS + @Deprecated(since = "CEP-21") INTERNAL_ADDRESS_AND_PORT, //derived from ClusterMetadata, Replacement for INTERNAL_IP with up to two ports + @Deprecated(since = "CEP-21") NATIVE_ADDRESS_AND_PORT, //derived from ClusterMetadata, Replacement for RPC_ADDRESS STATUS_WITH_PORT, //Replacement for STATUS /** * The set of sstable versions on this node. This will usually be only the "current" sstable format (the one with @@ -71,5 +71,5 @@ public enum ApplicationState X7, X8, X9, - X10, + X10; } diff --git a/src/java/org/apache/cassandra/gms/EndpointState.java b/src/java/org/apache/cassandra/gms/EndpointState.java index 49847a3c71..c96f99da2e 100644 --- a/src/java/org/apache/cassandra/gms/EndpointState.java +++ b/src/java/org/apache/cassandra/gms/EndpointState.java @@ -34,6 +34,7 @@ import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.NullableSerializer; @@ -69,10 +70,11 @@ public class EndpointState this(new HeartBeatState(other.hbState), new EnumMap<>(other.applicationState.get())); } - EndpointState(HeartBeatState initialHbState, Map states) + @VisibleForTesting + public EndpointState(HeartBeatState initialHbState, Map states) { hbState = initialHbState; - applicationState = new AtomicReference>(new EnumMap<>(states)); + applicationState = new AtomicReference<>(new EnumMap<>(states)); updateTimestamp = nanoTime(); isAlive = true; } @@ -237,11 +239,6 @@ public class EndpointState return rpcState != null && Boolean.parseBoolean(rpcState.value); } - public boolean isNormalState() - { - return getStatus().equals(VersionedValue.STATUS_NORMAL); - } - public String getStatus() { VersionedValue status = getApplicationState(ApplicationState.STATUS_WITH_PORT); @@ -262,10 +259,7 @@ public class EndpointState @Nullable public UUID getSchemaVersion() { - VersionedValue applicationState = getApplicationState(ApplicationState.SCHEMA); - return applicationState != null - ? UUID.fromString(applicationState.value) - : null; + return ClusterMetadata.current().schema.getVersion(); } @Nullable diff --git a/src/java/org/apache/cassandra/gms/FailureDetector.java b/src/java/org/apache/cassandra/gms/FailureDetector.java index 9cf7ad0392..ecaea5c24c 100644 --- a/src/java/org/apache/cassandra/gms/FailureDetector.java +++ b/src/java/org/apache/cassandra/gms/FailureDetector.java @@ -47,9 +47,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MBeanWrapper; @@ -254,15 +257,13 @@ public class FailureDetector implements IFailureDetector, FailureDetectorMBean continue; sb.append(" ").append(state.getKey()).append(":").append(state.getValue().version).append(":").append(state.getValue().value).append("\n"); } - VersionedValue tokens = endpointState.getApplicationState(ApplicationState.TOKENS); - if (tokens != null) - { - sb.append(" TOKENS:").append(tokens.version).append(":\n"); - } + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(FBUtilities.getBroadcastAddressAndPort()); + List tokens = metadata.tokenMap.tokens(nodeId); + if (tokens != null && !tokens.isEmpty()) + sb.append(" TOKENS:").append(metadata.epoch.getEpoch()).append(":\n"); else - { sb.append(" TOKENS: not present\n"); - } } /** @@ -305,7 +306,7 @@ public class FailureDetector implements IFailureDetector, FailureDetectorMBean // it's worth being defensive here so minor bugs don't cause disproportionate // badness. (See CASSANDRA-1463 for an example). if (epState == null) - logger.error("Unknown endpoint: " + ep, new IllegalArgumentException("")); + logger.error("Unknown endpoint: " + ep, new IllegalArgumentException("Unknown endpoint: " + ep)); return epState != null && epState.isAlive(); } diff --git a/src/java/org/apache/cassandra/gms/GossipDigestAck.java b/src/java/org/apache/cassandra/gms/GossipDigestAck.java index 26494eaba9..07feab29d7 100644 --- a/src/java/org/apache/cassandra/gms/GossipDigestAck.java +++ b/src/java/org/apache/cassandra/gms/GossipDigestAck.java @@ -41,7 +41,7 @@ public class GossipDigestAck final List gDigestList; final Map epStateMap; - GossipDigestAck(List gDigestList, Map epStateMap) + public GossipDigestAck(List gDigestList, Map epStateMap) { this.gDigestList = gDigestList; this.epStateMap = epStateMap; diff --git a/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java b/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java index 5fbe7ce0e3..084d41ed89 100644 --- a/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java +++ b/src/java/org/apache/cassandra/gms/GossipDigestAckVerbHandler.java @@ -42,7 +42,7 @@ public class GossipDigestAckVerbHandler extends GossipVerbHandler gDigestList = gDigestAckMessage.getGossipDigestList(); Map epStateMap = gDigestAckMessage.getEndpointStateMap(); logger.trace("Received ack with {} digests and {} states", gDigestList.size(), epStateMap.size()); - - if (Gossiper.instance.isInShadowRound()) + if (NewGossiper.instance.isInShadowRound()) { if (logger.isDebugEnabled()) logger.debug("Received an ack from {}, which may trigger exit from shadow round", from); - // if the ack is completely empty, then we can infer that the respondent is also in a shadow round - Gossiper.instance.maybeFinishShadowRound(from, gDigestList.isEmpty() && epStateMap.isEmpty(), epStateMap); - return; // don't bother doing anything else, we have what we came for + NewGossiper.instance.onAck(epStateMap); + return; } - if (epStateMap.size() > 0) { // Ignore any GossipDigestAck messages that we handle before a regular GossipDigestSyn has been send. diff --git a/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java b/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java index abaa39b14c..c2713863cc 100644 --- a/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java +++ b/src/java/org/apache/cassandra/gms/GossipDigestSynVerbHandler.java @@ -44,7 +44,7 @@ public class GossipDigestSynVerbHandler extends GossipVerbHandler gDigestList = gDigestMessage.getGossipDigests(); - // if the syn comes from a peer performing a shadow round and this node is // also currently in a shadow round, send back a minimal ack. This node must // be in the sender's seed list and doing this allows the sender to // differentiate between seeds from which it is partitioned and those which // are in their shadow round - if (!Gossiper.instance.isEnabled() && Gossiper.instance.isInShadowRound()) + if (!Gossiper.instance.isEnabled() && NewGossiper.instance.isInShadowRound()) { // a genuine syn (as opposed to one from a node currently // doing a shadow round) will always contain > 0 digests diff --git a/src/java/org/apache/cassandra/gms/Gossiper.java b/src/java/org/apache/cassandra/gms/Gossiper.java index 731965b65d..5ffa2d7bdb 100644 --- a/src/java/org/apache/cassandra/gms/Gossiper.java +++ b/src/java/org/apache/cassandra/gms/Gossiper.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -41,9 +42,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BooleanSupplier; -import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -51,36 +52,44 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Uninterruptibles; +import org.apache.cassandra.concurrent.FutureTask; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.compatibility.GossipHelper; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.Assassinate; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.ExecutorUtils; +import org.apache.cassandra.utils.MBeanWrapper; +import org.apache.cassandra.utils.NoSpamLogger; +import org.apache.cassandra.utils.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.concurrent.FutureTask; import org.apache.cassandra.concurrent.ScheduledExecutorPlus; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.RequestCallback; -import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.CassandraVersion; -import org.apache.cassandra.utils.ExecutorUtils; -import org.apache.cassandra.utils.ExpiringMemoizingSupplier; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JVMStabilityInspector; -import org.apache.cassandra.utils.MBeanWrapper; -import org.apache.cassandra.utils.NoSpamLogger; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.RecomputingSupplier; import org.apache.cassandra.utils.concurrent.NotScheduledFuture; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; @@ -93,7 +102,10 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.SHUTDOWN_A import static org.apache.cassandra.config.CassandraRelevantProperties.VERY_LONG_TIME_MS; import static org.apache.cassandra.config.DatabaseDescriptor.getClusterName; import static org.apache.cassandra.config.DatabaseDescriptor.getPartitionerName; +import static org.apache.cassandra.gms.Gossiper.GossipedWith.CMS; +import static org.apache.cassandra.gms.Gossiper.GossipedWith.SEED; import static org.apache.cassandra.gms.VersionedValue.BOOTSTRAPPING_STATUS; +import static org.apache.cassandra.gms.VersionedValue.unsafeMakeVersionedValue; import static org.apache.cassandra.net.NoPayload.noPayload; import static org.apache.cassandra.net.Verb.ECHO_REQ; import static org.apache.cassandra.net.Verb.GOSSIP_DIGEST_SYN; @@ -129,12 +141,8 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, static { SILENT_SHUTDOWN_STATES.addAll(DEAD_STATES); - SILENT_SHUTDOWN_STATES.add(VersionedValue.STATUS_BOOTSTRAPPING); - SILENT_SHUTDOWN_STATES.add(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE); } - private static final List ADMINISTRATIVELY_INACTIVE_STATES = Arrays.asList(VersionedValue.HIBERNATE, - VersionedValue.REMOVED_TOKEN, - VersionedValue.STATUS_LEFT); + private volatile ScheduledFuture scheduledGossipTask; private static final ReentrantLock taskLock = new ReentrantLock(); public final static int intervalInMillis = 1000; @@ -180,23 +188,8 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, private final Map expireTimeEndpointMap = new ConcurrentHashMap<>(); - private volatile boolean inShadowRound = false; - // seeds gathered during shadow round that indicated to be in the shadow round phase as well - private final Set seedsInShadowRound = new ConcurrentSkipListSet<>(); - // endpoint states as gathered during shadow round - private final Map endpointShadowStateMap = new ConcurrentHashMap<>(); - private volatile long lastProcessedMessageAt = currentTimeMillis(); - /** - * This property is initially set to {@code true} which means that we have no information about the other nodes. - * Once all nodes are on at least this node version, it becomes {@code false}, which means that we are not - * upgrading from the previous version (major, minor). - * - * This property and anything that checks it should be removed in 5.0 - */ - private volatile boolean upgradeInProgressPossible = true; - public void clearUnsafe() { unreachableEndpoints.clear(); @@ -204,66 +197,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, justRemovedEndpoints.clear(); expireTimeEndpointMap.clear(); endpointStateMap.clear(); - endpointShadowStateMap.clear(); - seedsInShadowRound.clear(); - } - - // returns true when the node does not know the existence of other nodes. - private static boolean isLoneNode(Map epStates) - { - return epStates.isEmpty() || epStates.keySet().equals(Collections.singleton(FBUtilities.getBroadcastAddressAndPort())); - } - - private static final ExpiringMemoizingSupplier.Memoized NO_UPGRADE_IN_PROGRESS = new ExpiringMemoizingSupplier.Memoized<>(null); - private static final ExpiringMemoizingSupplier.NotMemoized CURRENT_NODE_VERSION = new ExpiringMemoizingSupplier.NotMemoized<>(SystemKeyspace.CURRENT_VERSION); - final Supplier> upgradeFromVersionSupplier = () -> - { - // Once there are no prior version nodes we don't need to keep rechecking - if (!upgradeInProgressPossible) - return NO_UPGRADE_IN_PROGRESS; - - CassandraVersion minVersion = SystemKeyspace.CURRENT_VERSION; - - // Skip the round if the gossiper has not started yet - // Otherwise, upgradeInProgressPossible can be set to false wrongly. - // If we don't know any epstate we don't know anything about the cluster. - // If we only know about ourselves, we can assume that version is CURRENT_VERSION - if (!isEnabled() || isLoneNode(endpointStateMap)) - return CURRENT_NODE_VERSION; - - // Check the release version of all the peers it heard of. Not necessary the peer that it has/had contacted with. - boolean allHostsHaveKnownVersion = true; - for (InetAddressAndPort host : endpointStateMap.keySet()) - { - if (justRemovedEndpoints.containsKey(host)) - continue; - - CassandraVersion version = getReleaseVersion(host); - - //Raced with changes to gossip state, wait until next iteration - if (version == null) - allHostsHaveKnownVersion = false; - else if (version.compareTo(minVersion) < 0) - minVersion = version; - } - - if (minVersion.compareTo(SystemKeyspace.CURRENT_VERSION) < 0) - return new ExpiringMemoizingSupplier.Memoized<>(minVersion); - - if (!allHostsHaveKnownVersion) - return new ExpiringMemoizingSupplier.NotMemoized<>(minVersion); - - upgradeInProgressPossible = false; - return NO_UPGRADE_IN_PROGRESS; - }; - - private final Supplier upgradeFromVersionMemoized = ExpiringMemoizingSupplier.memoizeWithExpiration(upgradeFromVersionSupplier, 1, TimeUnit.MINUTES); - - @VisibleForTesting - public void expireUpgradeFromVersion() - { - upgradeInProgressPossible = true; - ((ExpiringMemoizingSupplier) upgradeFromVersionMemoized).expire(); } private static final boolean disableThreadValidation = GOSSIP_DISABLE_THREAD_VALIDATION.getBoolean(); @@ -301,6 +234,11 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } + public Map getEndpointStates() + { + return endpointStateMap; + } + private class GossipTask implements Runnable { public void run() @@ -327,7 +265,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, gDigests); Message message = Message.out(GOSSIP_DIGEST_SYN, digestSynMessage); /* Gossip to some random live member */ - boolean gossipedToSeed = doGossipToLiveMember(message); + EnumSet gossipedWith = doGossipToLiveMember(message); /* Gossip to some unreachable member with some probability to check if he is back up */ maybeGossipToUnreachableMember(message); @@ -348,8 +286,11 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, gossipedToSeed check. See CASSANDRA-150 for more exposition. */ - if (!gossipedToSeed || liveEndpoints.size() < seeds.size()) - maybeGossipToSeed(message); + if (!gossipedWith.contains(SEED) || liveEndpoints.size() < seeds.size()) + gossipedWith.addAll(maybeGossipToSeed(message)); + + if (!gossipedWith.contains(CMS)) + maybeGossipToCMS(message); doStatusCheck(); } @@ -366,8 +307,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } - private final RecomputingSupplier minVersionSupplier = new RecomputingSupplier<>(this::computeMinVersion, executor); - @VisibleForTesting public Gossiper(boolean registerJmx) { @@ -385,26 +324,18 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, subscribers.add(new IEndpointStateChangeSubscriber() { public void onJoin(InetAddressAndPort endpoint, EndpointState state) - { + { maybeRecompute(state); } public void onAlive(InetAddressAndPort endpoint, EndpointState state) - { + { maybeRecompute(state); } - private void maybeRecompute(EndpointState state) - { - if (state.getApplicationState(ApplicationState.RELEASE_VERSION) != null) - minVersionSupplier.recompute(); - } + private void maybeRecompute(EndpointState state) {} - public void onChange(InetAddressAndPort endpoint, ApplicationState state, VersionedValue value) - { - if (state == ApplicationState.RELEASE_VERSION) - minVersionSupplier.recompute(); - } + public void onChange(InetAddressAndPort endpoint, ApplicationState state, VersionedValue value) {} }); } @@ -413,36 +344,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, this.lastProcessedMessageAt = timeInMillis; } - public boolean seenAnySeed() - { - for (Map.Entry entry : endpointStateMap.entrySet()) - { - if (seeds.contains(entry.getKey())) - return true; - try - { - VersionedValue internalIp = entry.getValue().getApplicationState(ApplicationState.INTERNAL_IP); - VersionedValue internalIpAndPort = entry.getValue().getApplicationState(ApplicationState.INTERNAL_ADDRESS_AND_PORT); - InetAddressAndPort endpoint = null; - if (internalIpAndPort != null) - { - endpoint = InetAddressAndPort.getByName(internalIpAndPort.value); - } - else if (internalIp != null) - { - endpoint = InetAddressAndPort.getByName(internalIp.value); - } - if (endpoint != null && seeds.contains(endpoint)) - return true; - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } - return false; - } - /** * Register for interesting state changes. * @@ -498,12 +399,19 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, public Set getUnreachableTokenOwners() { Set tokenOwners = new HashSet<>(); + ClusterMetadata metadata = ClusterMetadata.current(); for (InetAddressAndPort endpoint : unreachableEndpoints.keySet()) { - if (StorageService.instance.getTokenMetadata().isMember(endpoint)) - tokenOwners.add(endpoint); + NodeId nodeId = metadata.directory.peerId(endpoint); + NodeState state = metadata.directory.peerState(nodeId); + switch (state) + { + case JOINED: + case MOVING: + case LEAVING: + tokenOwners.add(endpoint); + } } - return tokenOwners; } @@ -524,17 +432,23 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return false; } + return isShutdown(epState); + } + + private static boolean isShutdown(EndpointState epState) + { VersionedValue versionedValue = epState.getApplicationState(ApplicationState.STATUS_WITH_PORT); if (versionedValue == null) - { versionedValue = epState.getApplicationState(ApplicationState.STATUS); - if (versionedValue == null) - { - return false; - } - } + return isShutdown(versionedValue); + } - String value = versionedValue.value; + public static boolean isShutdown(VersionedValue vv) + { + if (vv == null) + return false; + + String value = vv.value; String[] pieces = value.split(VersionedValue.DELIMITER_STR, -1); assert (pieces.length > 0); String state = pieces[0]; @@ -549,7 +463,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, runnable.run(); return; } - FutureTask task = new FutureTask<>(runnable); Stage.GOSSIP.execute(task); try @@ -608,6 +521,8 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, EndpointState epState = endpointStateMap.get(endpoint); if (epState == null || epState.isStateEmpty()) return; + if (isShutdown(epState)) + return; VersionedValue shutdown = StorageService.instance.valueFactory.shutdown(true); epState.addApplicationState(ApplicationState.STATUS_WITH_PORT, shutdown); epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.shutdown(true)); @@ -666,7 +581,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, * * @param endpoint endpoint to be removed from the current membership. */ - private void evictFromMembership(InetAddressAndPort endpoint) + public void evictFromMembership(InetAddressAndPort endpoint) { checkProperThreadForStateMutation(); unreachableEndpoints.remove(endpoint); @@ -746,35 +661,13 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } /** - * Quarantine endpoint specifically for replacement purposes. - * @param endpoint - */ - public void replacementQuarantine(InetAddressAndPort endpoint) - { - // remember, quarantineEndpoint will effectively already add QUARANTINE_DELAY, so this is 2x - quarantineEndpoint(endpoint, currentTimeMillis() + QUARANTINE_DELAY); - GossiperDiagnostics.replacementQuarantine(this, endpoint); - } - - /** - * Remove the Endpoint and evict immediately, to avoid gossiping about this node. - * This should only be called when a token is taken over by a new IP address. + * The gossip digest is built based on randomization + * rather than just looping through the collection of live endpoints. * - * @param endpoint The endpoint that has been replaced + * @param gDigests list of Gossip Digests. */ - public void replacedEndpoint(InetAddressAndPort endpoint) - { - checkProperThreadForStateMutation(); - removeEndpoint(endpoint); - evictFromMembership(endpoint); - replacementQuarantine(endpoint); - GossiperDiagnostics.replacedEndpoint(this, endpoint); - } - - /** - * @param gDigests list of Gossip Digests to be filled - */ - private void makeGossipDigest(List gDigests) + @VisibleForTesting + void makeGossipDigest(List gDigests) { EndpointState epState; int generation; @@ -809,60 +702,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } - /** - * This method will begin removing an existing endpoint from the cluster by spoofing its state - * This should never be called unless this coordinator has had 'removenode' invoked - * - * @param endpoint - the endpoint being removed - * @param hostId - the ID of the host being removed - * @param localHostId - my own host ID for replication coordination - */ - public void advertiseRemoving(InetAddressAndPort endpoint, UUID hostId, UUID localHostId) - { - EndpointState epState = endpointStateMap.get(endpoint); - // remember this node's generation - int generation = epState.getHeartBeatState().getGeneration(); - logger.info("Removing host: {}", hostId); - logger.info("Sleeping for {}ms to ensure {} does not change", StorageService.RING_DELAY_MILLIS, endpoint); - Uninterruptibles.sleepUninterruptibly(StorageService.RING_DELAY_MILLIS, TimeUnit.MILLISECONDS); - // make sure it did not change - epState = endpointStateMap.get(endpoint); - if (epState.getHeartBeatState().getGeneration() != generation) - throw new RuntimeException("Endpoint " + endpoint + " generation changed while trying to remove it"); - // update the other node's generation to mimic it as if it had changed it itself - logger.info("Advertising removal for {}", endpoint); - epState.updateTimestamp(); // make sure we don't evict it too soon - epState.getHeartBeatState().forceNewerGenerationUnsafe(); - Map states = new EnumMap<>(ApplicationState.class); - states.put(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.removingNonlocal(hostId)); - states.put(ApplicationState.STATUS, StorageService.instance.valueFactory.removingNonlocal(hostId)); - states.put(ApplicationState.REMOVAL_COORDINATOR, StorageService.instance.valueFactory.removalCoordinator(localHostId)); - epState.addApplicationStates(states); - endpointStateMap.put(endpoint, epState); - } - - /** - * Handles switching the endpoint's state from REMOVING_TOKEN to REMOVED_TOKEN - * This should only be called after advertiseRemoving - * - * @param endpoint - * @param hostId - */ - public void advertiseTokenRemoved(InetAddressAndPort endpoint, UUID hostId) - { - EndpointState epState = endpointStateMap.get(endpoint); - epState.updateTimestamp(); // make sure we don't evict it too soon - epState.getHeartBeatState().forceNewerGenerationUnsafe(); - long expireTime = computeExpireTime(); - epState.addApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.removedNonlocal(hostId, expireTime)); - epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.removedNonlocal(hostId, expireTime)); - logger.info("Completing removal of {}", endpoint); - addExpireTimeForEndpoint(endpoint, expireTime); - endpointStateMap.put(endpoint, epState); - // ensure at least one gossip round occurs before returning - Uninterruptibles.sleepUninterruptibly(intervalInMillis * 2, TimeUnit.MILLISECONDS); - } - public void unsafeAssassinateEndpoint(String address) throws UnknownHostException { logger.warn("Gossiper.unsafeAssassinateEndpoint is deprecated and will be removed in the next release; use assassinateEndpoint instead"); @@ -880,54 +719,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, public void assassinateEndpoint(String address) throws UnknownHostException { InetAddressAndPort endpoint = InetAddressAndPort.getByName(address); - runInGossipStageBlocking(() -> { - EndpointState epState = endpointStateMap.get(endpoint); - logger.warn("Assassinating {} via gossip", endpoint); - - if (epState == null) - { - epState = new EndpointState(new HeartBeatState((int) ((currentTimeMillis() + 60000) / 1000), 9999)); - } - else - { - int generation = epState.getHeartBeatState().getGeneration(); - int heartbeat = epState.getHeartBeatState().getHeartBeatVersion(); - logger.info("Sleeping for {}ms to ensure {} does not change", StorageService.RING_DELAY_MILLIS, endpoint); - Uninterruptibles.sleepUninterruptibly(StorageService.RING_DELAY_MILLIS, TimeUnit.MILLISECONDS); - // make sure it did not change - EndpointState newState = endpointStateMap.get(endpoint); - if (newState == null) - logger.warn("Endpoint {} disappeared while trying to assassinate, continuing anyway", endpoint); - else if (newState.getHeartBeatState().getGeneration() != generation) - throw new RuntimeException("Endpoint still alive: " + endpoint + " generation changed while trying to assassinate it"); - else if (newState.getHeartBeatState().getHeartBeatVersion() != heartbeat) - throw new RuntimeException("Endpoint still alive: " + endpoint + " heartbeat changed while trying to assassinate it"); - epState.updateTimestamp(); // make sure we don't evict it too soon - epState.getHeartBeatState().forceNewerGenerationUnsafe(); - } - - Collection tokens = null; - try - { - tokens = StorageService.instance.getTokenMetadata().getTokens(endpoint); - } - catch (Throwable th) - { - JVMStabilityInspector.inspectThrowable(th); - } - if (tokens == null || tokens.isEmpty()) - { - logger.warn("Trying to assassinate an endpoint {} that does not have any tokens assigned. This should not have happened, trying to continue with a random token.", address); - tokens = Collections.singletonList(StorageService.instance.getTokenMetadata().partitioner.getRandomToken()); - } - - long expireTime = computeExpireTime(); - epState.addApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.left(tokens, expireTime)); - epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.left(tokens, computeExpireTime())); - handleMajorStateChange(endpoint, epState); - Uninterruptibles.sleepUninterruptibly(intervalInMillis * 4, TimeUnit.MILLISECONDS); - logger.warn("Finished assassinating {}", endpoint); - }); + Assassinate.assassinateEndpoint(endpoint); } public boolean isKnownEndpoint(InetAddressAndPort endpoint) @@ -947,13 +739,13 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, * @param epSet a set of endpoint from which a random endpoint is chosen. * @return true if the chosen endpoint is also a seed. */ - private boolean sendGossip(Message message, Set epSet) + private EnumSet sendGossip(Message message, Set epSet) { List endpoints = ImmutableList.copyOf(epSet); int size = endpoints.size(); if (size < 1) - return false; + return EnumSet.noneOf(GossipedWith.class); /* Generate a random number from 0 -> size */ int index = (size == 1) ? 0 : random.nextInt(size); InetAddressAndPort to = endpoints.get(index); @@ -962,21 +754,31 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, if (firstSynSendAt == 0) firstSynSendAt = nanoTime(); MessagingService.instance().send(message, to); + EnumSet gossipedWith = EnumSet.noneOf(GossipedWith.class); - boolean isSeed = seeds.contains(to); + if (seeds.contains(to)) + gossipedWith.add(SEED); + if (ClusterMetadata.current().fullCMSMembers().contains(to)) + gossipedWith.add(CMS); GossiperDiagnostics.sendGossipDigestSyn(this, to); - return isSeed; + return gossipedWith; } /* Sends a Gossip message to a live member and returns true if the recipient was a seed */ - private boolean doGossipToLiveMember(Message message) + private EnumSet doGossipToLiveMember(Message message) { int size = liveEndpoints.size(); if (size == 0) - return false; + return EnumSet.noneOf(GossipedWith.class); return sendGossip(message, liveEndpoints); } + enum GossipedWith + { + SEED, + CMS + } + /* Sends a Gossip message to an unreachable member */ private void maybeGossipToUnreachableMember(Message message) { @@ -996,19 +798,20 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } /* Possibly gossip to a seed for facilitating partition healing */ - private void maybeGossipToSeed(Message prod) + private EnumSet maybeGossipToSeed(Message prod) { int size = seeds.size(); + EnumSet gossipedWith = EnumSet.noneOf(GossipedWith.class); if (size > 0) { if (size == 1 && seeds.contains(getBroadcastAddressAndPort())) { - return; + return gossipedWith; } if (liveEndpoints.size() == 0) { - sendGossip(prod, seeds); + gossipedWith = sendGossip(prod, seeds); } else { @@ -1016,9 +819,25 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, double probability = seeds.size() / (double) (liveEndpoints.size() + unreachableEndpoints.size()); double randDbl = random.nextDouble(); if (randDbl <= probability) - sendGossip(prod, seeds); + gossipedWith = sendGossip(prod, seeds); } } + return gossipedWith; + } + + private void maybeGossipToCMS(Message message) + { + Set cms = ClusterMetadata.current().fullCMSMembers(); + if (cms.contains(getBroadcastAddressAndPort())) + return; + + double probability = cms.size() / (double) (liveEndpoints.size() + unreachableEndpoints.size()); + double randDbl = random.nextDouble(); + if (randDbl <= probability) + { + logger.trace("Sending GossipDigestSyn to the CMS {}", cms); + sendGossip(message, cms); + } } public boolean isGossipOnlyMember(InetAddressAndPort endpoint) @@ -1028,64 +847,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, { return false; } - return !isDeadState(epState) && !StorageService.instance.getTokenMetadata().isMember(endpoint); - } - - /** - * Check if this node can safely be started and join the ring. - * If the node is bootstrapping, examines gossip state for any previous status to decide whether - * it's safe to allow this node to start and bootstrap. If not bootstrapping, compares the host ID - * that the node itself has (obtained by reading from system.local or generated if not present) - * with the host ID obtained from gossip for the endpoint address (if any). This latter case - * prevents a non-bootstrapping, new node from being started with the same address of a - * previously started, but currently down predecessor. - * - * @param endpoint - the endpoint to check - * @param localHostUUID - the host id to check - * @param isBootstrapping - whether the node intends to bootstrap when joining - * @param epStates - endpoint states in the cluster - * @return true if it is safe to start the node, false otherwise - */ - public boolean isSafeForStartup(InetAddressAndPort endpoint, UUID localHostUUID, boolean isBootstrapping, - Map epStates) - { - EndpointState epState = epStates.get(endpoint); - // if there's no previous state, we're good - if (epState == null) - return true; - - String status = getGossipStatus(epState); - - if (status.equals(VersionedValue.HIBERNATE) - && !SystemKeyspace.bootstrapComplete()) - { - logger.warn("A node with the same IP in hibernate status was detected. Was a replacement already attempted?"); - return false; - } - - //the node was previously removed from the cluster - if (isDeadState(epState)) - return true; - - if (isBootstrapping) - { - // these states are not allowed to join the cluster as it would not be safe - final List unsafeStatuses = new ArrayList() - {{ - add(""); // failed bootstrap but we did start gossiping - add(VersionedValue.STATUS_NORMAL); // node is legit in the cluster or it was stopped with kill -9 - add(VersionedValue.SHUTDOWN); // node was shutdown - }}; - return !unsafeStatuses.contains(status); - } - else - { - // if the previous UUID matches what we currently have (i.e. what was read from - // system.local at startup), then we're good to start up. Otherwise, something - // is amiss and we need to replace the previous node - VersionedValue previous = epState.getApplicationState(ApplicationState.HOST_ID); - return UUID.fromString(previous.value).equals(localHostUUID); - } + return !isDeadState(epState) && !ClusterMetadata.current().directory.allJoinedEndpoints().contains(endpoint); } @VisibleForTesting @@ -1111,6 +873,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } + ClusterMetadata metadata = ClusterMetadata.current(); Set eps = endpointStateMap.keySet(); for (InetAddressAndPort endpoint : eps) { @@ -1136,7 +899,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, // to make sure that the previous read data was correct logger.info("Race condition marking {} as a FatClient; ignoring", endpoint); return; - } + } removeEndpoint(endpoint); // will put it in justRemovedEndpoints to respect quarantine delay evictFromMembership(endpoint); // can get rid of the state immediately }); @@ -1145,7 +908,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, // check for dead state removal long expireTime = getExpireTimeForEndpoint(endpoint); if (!epState.isAlive() && (now > expireTime) - && (!StorageService.instance.getTokenMetadata().isMember(endpoint))) + && (!metadata.directory.allAddresses().contains(endpoint))) { if (logger.isDebugEnabled()) { @@ -1191,20 +954,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return new EndpointState(epState); } - public ImmutableSet getEndpoints() - { - return ImmutableSet.copyOf(endpointStateMap.keySet()); - } - - public String getForEndpoint(InetAddressAndPort ep, ApplicationState state) - { - EndpointState epState = endpointStateMap.get(ep); - if (epState == null) - return null; - VersionedValue value = epState.getApplicationState(state); - return value == null ? null : value.value; - } - public int getEndpointCount() { return endpointStateMap.size(); @@ -1225,26 +974,11 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return ImmutableMap.copyOf(unreachableEndpoints); } - Set getSeedsInShadowRound() - { - return ImmutableSet.copyOf(seedsInShadowRound); - } - long getLastProcessedMessageAt() { return lastProcessedMessageAt; } - public UUID getHostId(InetAddressAndPort endpoint) - { - return getHostId(endpoint, endpointStateMap); - } - - public UUID getHostId(InetAddressAndPort endpoint, Map epStates) - { - return UUID.fromString(epStates.get(endpoint).getApplicationState(ApplicationState.HOST_ID).value); - } - /** * The value for the provided application state for the provided endpoint as currently known by this Gossip instance. * @@ -1311,17 +1045,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return reqdEndpointState; } - /** - * determine which endpoint started up earlier - */ - public int compareEndpointStartup(InetAddressAndPort addr1, InetAddressAndPort addr2) - { - EndpointState ep1 = getEndpointStateForEndpoint(addr1); - EndpointState ep2 = getEndpointStateForEndpoint(addr2); - assert ep1 != null && ep2 != null; - return ep1.getHeartBeatState().getGeneration() - ep2.getHeartBeatState().getGeneration(); - } - public void notifyFailureDetector(Map remoteEpStateMap) { for (Entry entry : remoteEpStateMap.entrySet()) @@ -1419,6 +1142,10 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, logger.trace("marking as down {}", addr); silentlyMarkDead(addr, localState); logger.info("InetAddress {} is now DOWN", addr); + + // if the node isn't registered, don't notify + if (ClusterMetadata.current().directory.peerId(addr) == null) + return; for (IEndpointStateChangeSubscriber subscriber : subscribers) subscriber.onDead(addr, localState); if (logger.isTraceEnabled()) @@ -1428,7 +1155,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } /** - * Used by {@link #markDead(InetAddressAndPort, EndpointState)} and {@link #addSavedEndpoint(InetAddressAndPort)} + * Used by {@link #markDead(InetAddressAndPort, EndpointState)} * to register a endpoint as dead. This method is "silent" to avoid triggering listeners, diagnostics, or logs * on startup via addSavedEndpoint. */ @@ -1454,6 +1181,8 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, EndpointState localEpState = endpointStateMap.get(ep); if (!isDeadState(epState)) { + // confusing log message, epState status might still be 'shutdown' - keeping if anyone is using it for automation + // we're not actually marking it as up until we get the echo request response in markAlive below if (localEpState != null) logger.info("Node {} has restarted, now UP", ep); else @@ -1511,23 +1240,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return SILENT_SHUTDOWN_STATES.contains(status); } - public boolean isAdministrativelyInactiveState(EndpointState epState) - { - String status = getGossipStatus(epState); - if (status.isEmpty()) - return false; - - return ADMINISTRATIVELY_INACTIVE_STATES.contains(status); - } - - public boolean isAdministrativelyInactiveState(InetAddressAndPort endpoint) - { - EndpointState epState = getEndpointStateForEndpoint(endpoint); - if (epState == null) - return true; // if the end point cannot be found, treat as inactive - return isAdministrativelyInactiveState(epState); - } - public static String getGossipStatus(EndpointState epState) { if (epState == null) @@ -1621,11 +1333,10 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, public void applyStateLocally(Map epStateMap) { checkProperThreadForStateMutation(); - boolean hasMajorVersion3Nodes = hasMajorVersion3Nodes(); for (Entry entry : order(epStateMap)) { InetAddressAndPort ep = entry.getKey(); - if (ep.equals(getBroadcastAddressAndPort()) && !isInShadowRound()) + if (ep.equals(getBroadcastAddressAndPort())) continue; if (justRemovedEndpoints.containsKey(ep)) { @@ -1636,8 +1347,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, EndpointState localEpStatePtr = endpointStateMap.get(ep); EndpointState remoteState = entry.getValue(); - if (!hasMajorVersion3Nodes) - remoteState.removeMajorVersion3LegacyApplicationStates(); + remoteState.removeMajorVersion3LegacyApplicationStates(); /* If state does not exist just add it. If it does then add it if the remote generation is greater. @@ -1672,10 +1382,10 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, if (remoteMaxVersion > localMaxVersion) { // apply states, but do not notify since there is no major change - applyNewStates(ep, localEpStatePtr, remoteState, hasMajorVersion3Nodes); + applyNewStates(ep, localEpStatePtr, remoteState); } else if (logger.isTraceEnabled()) - logger.trace("Ignoring remote version {} <= {} for {}", remoteMaxVersion, localMaxVersion, ep); + logger.trace("Ignoring remote version {} <= {} for {}", remoteMaxVersion, localMaxVersion, ep); if (!localEpStatePtr.isAlive() && !isDeadState(localEpStatePtr)) // unless of course, it was dead markAlive(ep, localEpStatePtr); @@ -1695,7 +1405,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } - private void applyNewStates(InetAddressAndPort addr, EndpointState localState, EndpointState remoteState, boolean hasMajorVersion3Nodes) + private void applyNewStates(InetAddressAndPort addr, EndpointState localState, EndpointState remoteState) { // don't assert here, since if the node restarts the version will go back to zero int oldVersion = localState.getHeartBeatState().getHeartBeatVersion(); @@ -1707,7 +1417,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, Set> remoteStates = remoteState.states(); assert remoteState.getHeartBeatState().getGeneration() == localState.getHeartBeatState().getGeneration(); - Set> updatedStates = remoteStates.stream().filter(entry -> { // filter out the states that are already up to date (has the same or higher version) VersionedValue local = localState.getApplicationState(entry.getKey()); @@ -1722,10 +1431,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } localState.addApplicationStates(updatedStates); - - // get rid of legacy fields once the cluster is not in mixed mode - if (!hasMajorVersion3Nodes) - localState.removeMajorVersion3LegacyApplicationStates(); + localState.removeMajorVersion3LegacyApplicationStates(); // need to run STATUS or STATUS_WITH_PORT first to handle BOOT_REPLACE correctly (else won't be a member, so TOKENS won't be processed) for (Entry updatedEntry : updatedStates) @@ -1839,13 +1545,13 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, if (null != hostId) { state.addApplicationState(ApplicationState.HOST_ID, - StorageService.instance.valueFactory.hostId(hostId)); + StorageService.instance.valueFactory.hostId(hostId)); } Set tokens = SystemKeyspace.loadTokens().get(endpoint); if (null != tokens && !tokens.isEmpty()) { state.addApplicationState(ApplicationState.TOKENS, - StorageService.instance.valueFactory.tokens(tokens)); + StorageService.instance.valueFactory.tokens(tokens)); } } map.put(endpoint, state); @@ -1921,123 +1627,31 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, public void start(int generationNumber) { - start(generationNumber, new EnumMap<>(ApplicationState.class)); + start(generationNumber, false); } /** * Start the gossiper with the generation number, preloading the map of application states before starting */ - public void start(int generationNbr, Map preloadLocalStates) + public void start(int generationNbr, boolean mergeLocalStates) { buildSeedsList(); /* initialize the heartbeat state for this localEndpoint */ maybeInitializeLocalState(generationNbr); - EndpointState localState = endpointStateMap.get(getBroadcastAddressAndPort()); - localState.addApplicationStates(preloadLocalStates); - minVersionSupplier.recompute(); + ClusterMetadata metadata = ClusterMetadata.current(); + if (mergeLocalStates && metadata.myNodeId() != null) + mergeNodeToGossip(metadata.myNodeId(), metadata); //notify snitches that Gossiper is about to start DatabaseDescriptor.getEndpointSnitch().gossiperStarting(); - if (logger.isTraceEnabled()) - logger.trace("gossip started with generation {}", localState.getHeartBeatState().getGeneration()); + shutdownAnnounced.set(false); scheduledGossipTask = executor.scheduleWithFixedDelay(new GossipTask(), Gossiper.intervalInMillis, Gossiper.intervalInMillis, TimeUnit.MILLISECONDS); } - public synchronized Map doShadowRound() - { - return doShadowRound(Collections.EMPTY_SET); - } - - /** - * Do a single 'shadow' round of gossip by retrieving endpoint states that will be stored exclusively in the - * map return value, instead of endpointStateMap. - * - * Used when preparing to join the ring: - *
      - *
    • when replacing a node, to get and assume its tokens
    • - *
    • when joining, to check that the local host id matches any previous id for the endpoint address
    • - *
    - * - * Method is synchronized, as we use an in-progress flag to indicate that shadow round must be cleared - * again by calling {@link Gossiper#maybeFinishShadowRound(InetAddressAndPort, boolean, Map)}. This will update - * {@link Gossiper#endpointShadowStateMap} with received values, in order to return an immutable copy to the - * caller of {@link Gossiper#doShadowRound()}. Therefor only a single shadow round execution is permitted at - * the same time. - * - * @param peers Additional peers to try gossiping with. - * @return endpoint states gathered during shadow round or empty map - */ - public synchronized Map doShadowRound(Set peers) - { - buildSeedsList(); - // it may be that the local address is the only entry in the seed + peers - // list in which case, attempting a shadow round is pointless - if (seeds.isEmpty() && peers.isEmpty()) - return endpointShadowStateMap; - - boolean isSeed = DatabaseDescriptor.getSeeds().contains(getBroadcastAddressAndPort()); - // We double RING_DELAY if we're not a seed to increase chance of successful startup during a full cluster bounce, - // giving the seeds a chance to startup before we fail the shadow round - int shadowRoundDelay = isSeed ? StorageService.RING_DELAY_MILLIS : StorageService.RING_DELAY_MILLIS * 2; - seedsInShadowRound.clear(); - endpointShadowStateMap.clear(); - // send a completely empty syn - List gDigests = new ArrayList<>(); - GossipDigestSyn digestSynMessage = new GossipDigestSyn(getClusterName(), getPartitionerName(), gDigests); - Message message = Message.out(GOSSIP_DIGEST_SYN, digestSynMessage); - - inShadowRound = true; - boolean includePeers = false; - int slept = 0; - try - { - while (true) - { - if (slept % 5000 == 0) - { // CASSANDRA-8072, retry at the beginning and every 5 seconds - logger.trace("Sending shadow round GOSSIP DIGEST SYN to seeds {}", seeds); - - for (InetAddressAndPort seed : seeds) - MessagingService.instance().send(message, seed); - - // Send to any peers we already know about, but only if a seed didn't respond. - if (includePeers) - { - logger.trace("Sending shadow round GOSSIP DIGEST SYN to known peers {}", peers); - for (InetAddressAndPort peer : peers) - MessagingService.instance().send(message, peer); - } - includePeers = true; - } - - Thread.sleep(1000); - if (!inShadowRound) - break; - - slept += 1000; - if (slept > shadowRoundDelay) - { - // if we got here no peers could be gossiped to. If we're a seed that's OK, but otherwise we stop. See CASSANDRA-13851 - if (!isSeed) - throw new RuntimeException("Unable to gossip with any peers"); - - inShadowRound = false; - break; - } - } - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } - - return ImmutableMap.copyOf(endpointShadowStateMap); - } - @VisibleForTesting void buildSeedsList() { @@ -2125,45 +1739,14 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, epstate.getHeartBeatState().forceNewerGenerationUnsafe(); } - - /** - * Add an endpoint we knew about previously, but whose state is unknown - */ - public void addSavedEndpoint(InetAddressAndPort ep) - { - checkProperThreadForStateMutation(); - if (ep.equals(getBroadcastAddressAndPort())) - { - logger.debug("Attempt to add self as saved endpoint"); - return; - } - - //preserve any previously known, in-memory data about the endpoint (such as DC, RACK, and so on) - EndpointState epState = endpointStateMap.get(ep); - if (epState != null) - { - logger.debug("not replacing a previous epState for {}, but reusing it: {}", ep, epState); - epState.setHeartBeatState(HeartBeatState.empty()); - } - else - { - epState = new EndpointState(HeartBeatState.empty()); - logger.info("Adding {} as there was no previous epState; new state is {}", ep, epState); - } - - epState.markDead(); - endpointStateMap.put(ep, epState); - silentlyMarkDead(ep, epState); - if (logger.isTraceEnabled()) - logger.trace("Adding saved endpoint {} {}", ep, epState.getHeartBeatState().getGeneration()); - } - private void addLocalApplicationStateInternal(ApplicationState state, VersionedValue value) { assert taskLock.isHeldByCurrentThread(); InetAddressAndPort epAddr = getBroadcastAddressAndPort(); EndpointState epState = endpointStateMap.get(epAddr); - assert epState != null : "Can't find endpoint state for " + epAddr; + // todo; this can be null during startup log replay when bootstrapping + // - we would have no state for ourselves + if (epState == null) return; // Fire "before change" notifications: doBeforeChangeNotifications(epAddr, epState, state, value); // Notifications may have taken some time, so preventively raise the version @@ -2197,12 +1780,24 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } + /** + * To avoid racing with ECHO requests, we need to make sure to establish happens-before relation between + * announcing shutdown and responding to heartbeats. Once we are about to send the shutdown message, we + * should not respond to heartbeats anymore. + * + * Unfortunately, there are some tests that use FD and have gossip handlers, but do not use Gossip feature. + * To avoid reworking those, we rely on this atomic boolean rather than isEnabled to achieve this. + */ + public AtomicBoolean shutdownAnnounced = new AtomicBoolean(false); + public void stop() { EndpointState mystate = endpointStateMap.get(getBroadcastAddressAndPort()); if (mystate != null && !isSilentShutdownState(mystate) && StorageService.instance.isJoined()) { logger.info("Announcing shutdown"); + shutdownAnnounced.set(true); + addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.shutdown(true)); addLocalApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.shutdown(true)); Message message = Message.out(Verb.GOSSIP_SHUTDOWN, new GossipShutdown(mystate)); @@ -2222,78 +1817,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return (scheduledGossipTask != null) && (!scheduledGossipTask.isCancelled()); } - public boolean sufficientForStartupSafetyCheck(Map epStateMap) - { - // it is possible for a previously queued ack to be sent to us when we come back up in shadow - EndpointState localState = epStateMap.get(FBUtilities.getBroadcastAddressAndPort()); - // return false if response doesn't contain state necessary for safety check - return localState == null || isDeadState(localState) || localState.containsApplicationState(ApplicationState.HOST_ID); - } - - protected void maybeFinishShadowRound(InetAddressAndPort respondent, boolean isInShadowRound, Map epStateMap) - { - if (inShadowRound) - { - if (!isInShadowRound) - { - if (!sufficientForStartupSafetyCheck(epStateMap)) - { - logger.debug("Not exiting shadow round because received ACK with insufficient states {} -> {}", - FBUtilities.getBroadcastAddressAndPort(), epStateMap.get(FBUtilities.getBroadcastAddressAndPort())); - return; - } - - if (!seeds.contains(respondent)) - logger.warn("Received an ack from {}, who isn't a seed. Ensure your seed list includes a live node. Exiting shadow round", - respondent); - logger.debug("Received a regular ack from {}, can now exit shadow round", respondent); - // respondent sent back a full ack, so we can exit our shadow round - endpointShadowStateMap.putAll(epStateMap); - inShadowRound = false; - seedsInShadowRound.clear(); - } - else - { - // respondent indicates it too is in a shadow round, if all seeds - // are in this state then we can exit our shadow round. Otherwise, - // we keep retrying the SR until one responds with a full ACK or - // we learn that all seeds are in SR. - logger.debug("Received an ack from {} indicating it is also in shadow round", respondent); - seedsInShadowRound.add(respondent); - if (seedsInShadowRound.containsAll(seeds)) - { - logger.debug("All seeds are in a shadow round, clearing this node to exit its own"); - inShadowRound = false; - seedsInShadowRound.clear(); - } - } - } - } - - public boolean isInShadowRound() - { - return inShadowRound; - } - - /** - * Creates a new dead {@link EndpointState} that is {@link EndpointState#isEmptyWithoutStatus() empty}. This is used during - * host replacement for edge cases where the seed notified that the endpoint was empty, so need to add such state - * into gossip explicitly (as empty endpoints are not gossiped outside of the shadow round). - * - * see CASSANDRA-16213 - */ - public void initializeUnreachableNodeUnsafe(InetAddressAndPort addr) - { - EndpointState state = new EndpointState(HeartBeatState.empty()); - state.markDead(); - EndpointState oldState = endpointStateMap.putIfAbsent(addr, state); - if (null != oldState) - { - throw new RuntimeException("Attempted to initialize endpoint state for unreachable node, " + - "but found existing endpoint state for it."); - } - } - @VisibleForTesting public void initializeNodeUnsafe(InetAddressAndPort addr, UUID uuid, int generationNbr) { @@ -2378,6 +1901,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return state != null ? state.getSchemaVersion() : null; } + // TODO: (TM/alexp): we do not need to wait for gossip to settle anymore, since main keys are now coming from TM public static void waitToSettle() { int forceAfter = GOSSIPER_SKIP_WAITING_TO_SETTLE.getInt(); @@ -2385,6 +1909,18 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, { return; } + // Previously gossip contained only nodes that were actually in the cluster. Now we + // initialize gossip with nodes that may be down. If we do not add the initial marker, + // they will never be marked as up. + Directory directory = ClusterMetadata.current().directory; + for (InetAddressAndPort peer : directory.allJoinedEndpoints()) + { + if (!FBUtilities.getBroadcastAddressAndPort().equals(peer)) + { + FailureDetector.instance.report(peer); + FailureDetector.instance.forceConviction(peer); + } + } final int GOSSIP_SETTLE_MIN_WAIT_MS = CassandraRelevantProperties.GOSSIP_SETTLE_MIN_WAIT_MS.getInt(); final int GOSSIP_SETTLE_POLL_INTERVAL_MS = CassandraRelevantProperties.GOSSIP_SETTLE_POLL_INTERVAL_MS.getInt(); final int GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED = CassandraRelevantProperties.GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED.getInt(); @@ -2401,7 +1937,7 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, totalPolls++; if (currentSize == epSize) { - logger.debug("Gossip looks settled."); + logger.debug("Gossip looks settled. {}", Gossiper.instance.endpointStateMap); numOkay++; } else @@ -2430,6 +1966,8 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, * @param unit TimeUnit of maxWait * @return true if agreement was reached, false if not */ + // TODO: (TM/alexp): we do not need to wait for schema convergence for the purpose of view building; + // we will rely on different mechanisms for propagating mutations correctly public boolean waitForSchemaAgreement(long maxWait, TimeUnit unit, BooleanSupplier abortCondition) { int waited = 0; @@ -2451,30 +1989,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, } } - /** - * Returns {@code false} only if the information about the version of each node in the cluster is available and - * ALL the nodes are on 4.0+ (regardless of the patch version). - */ - public boolean hasMajorVersion3Nodes() - { - return isUpgradingFromVersionLowerThan(CassandraVersion.CASSANDRA_4_0) || // this is quite obvious - // however if we discovered only nodes at current version so far (in particular only this node), - // but still there are nodes with unknown version, we also want to report that the cluster may have nodes at 3.x - upgradeInProgressPossible && !isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION.familyLowerBound.get()); - } - - /** - * Returns {@code true} if there are nodes on version lower than the provided version - */ - public boolean isUpgradingFromVersionLowerThan(CassandraVersion referenceVersion) - { - CassandraVersion v = upgradeFromVersionMemoized.get(); - if (CassandraVersion.NULL_VERSION.equals(v) && scheduledGossipTask == null) - return false; - - return v != null && v.compareTo(referenceVersion) < 0; - } - private boolean nodesAgreeOnSchema(Collection nodes) { UUID expectedVersion = null; @@ -2501,26 +2015,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, ExecutorUtils.shutdownAndWait(timeout, unit, executor); } - @Nullable - public CassandraVersion getMinVersion(long delay, TimeUnit timeUnit) - { - try - { - return minVersionSupplier.get(delay, timeUnit); - } - catch (TimeoutException e) - { - // Timeouts here are harmless: they won't cause reprepares and may only - // cause the old version of the hash to be kept for longer - return null; - } - catch (Throwable e) - { - logger.error("Caught an exception while waiting for min version", e); - return null; - } - } - @Nullable private String getReleaseVersionString(InetAddressAndPort ep) { @@ -2532,41 +2026,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, return value == null ? null : value.value; } - private CassandraVersion computeMinVersion() - { - CassandraVersion minVersion = null; - - for (InetAddressAndPort addr : Iterables.concat(Gossiper.instance.getLiveMembers(), - Gossiper.instance.getUnreachableMembers())) - { - String versionString = getReleaseVersionString(addr); - // Raced with changes to gossip state, wait until next iteration - if (versionString == null) - return null; - - CassandraVersion version; - - try - { - version = new CassandraVersion(versionString); - } - catch (Throwable t) - { - JVMStabilityInspector.inspectThrowable(t); - String message = String.format("Can't parse version string %s", versionString); - logger.warn(message); - if (logger.isDebugEnabled()) - logger.debug(message, t); - return null; - } - - if (minVersion == null || version.compareTo(minVersion) < 0) - minVersion = version; - } - - return minVersion; - } - @Override public boolean getLooseEmptyEnabled() { @@ -2586,49 +2045,6 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, firstSynSendAt = 1; } - public Collection unsafeClearRemoteState() - { - List removed = new ArrayList<>(); - for (InetAddressAndPort ep : endpointStateMap.keySet()) - { - if (ep.equals(getBroadcastAddressAndPort())) - continue; - - for (IEndpointStateChangeSubscriber subscriber : subscribers) - subscriber.onRemove(ep); - - removed.add(ep); - } - this.endpointStateMap.keySet().retainAll(Collections.singleton(getBroadcastAddressAndPort())); - this.endpointShadowStateMap.keySet().retainAll(Collections.singleton(getBroadcastAddressAndPort())); - this.expireTimeEndpointMap.keySet().retainAll(Collections.singleton(getBroadcastAddressAndPort())); - this.justRemovedEndpoints.keySet().retainAll(Collections.singleton(getBroadcastAddressAndPort())); - this.unreachableEndpoints.keySet().retainAll(Collections.singleton(getBroadcastAddressAndPort())); - return removed; - } - - public void unsafeGossipWith(InetAddressAndPort ep) - { - /* Update the local heartbeat counter. */ - EndpointState epState = endpointStateMap.get(getBroadcastAddressAndPort()); - if (epState != null) - { - epState.getHeartBeatState().updateHeartBeat(); - if (logger.isTraceEnabled()) - logger.trace("My heartbeat is now {}", epState.getHeartBeatState().getHeartBeatVersion()); - } - - final List gDigests = new ArrayList(); - Gossiper.instance.makeGossipDigest(gDigests); - - GossipDigestSyn digestSynMessage = new GossipDigestSyn(getClusterName(), - getPartitionerName(), - gDigests); - Message message = Message.out(GOSSIP_DIGEST_SYN, digestSynMessage); - - MessagingService.instance().send(message, ep); - } - public void unsafeSendShutdown(InetAddressAndPort to) { Message message = Message.out(Verb.GOSSIP_SHUTDOWN, noPayload); @@ -2646,4 +2062,153 @@ public class Gossiper implements IFailureDetectionEventListener, GossiperMBean, Message message = Message.out(Verb.GOSSIP_DIGEST_ACK2, digestAck2Message); MessagingService.instance().send(message, ep); } + + private void unsafeUpdateEpStates(InetAddressAndPort endpoint, EndpointState epstate) + { + checkProperThreadForStateMutation(); + assert !endpoint.equals(getBroadcastAddressAndPort()) || epstate.getHeartBeatState().getGeneration() > 0 : + "We should not update epstates with generation = 0 for the local host"; + EndpointState old = endpointStateMap.get(endpoint); + if (old == null) + endpointStateMap.put(endpoint, epstate); + else + old.addApplicationStates(epstate.states()); + + if (!getBroadcastAddressAndPort().equals(endpoint)) + { + // don't consider it a major state change if the generation is 0 - this means we have only added it locally for a remote node + if (epstate.getHeartBeatState().getGeneration() > 0 && + (old == null || old.getHeartBeatState().getGeneration() < epstate.getHeartBeatState().getGeneration())) + handleMajorStateChange(endpoint, epstate); + } + } + + + /** + * Basic idea is that we can't ever bump the generation or version for a remote node + * + * If the remote node is not yet known, set generation and version to 0 to make sure that we don't overwrite + * any state generated by the remote node itself + * + * If the remote node is known, keep the remote generation and version and just update the versioned value in + * place, this makes sure that if the remote node changed, those values will override anything we have here. + */ + public void mergeNodeToGossip(NodeId nodeId, ClusterMetadata metadata) + { + mergeNodeToGossip(nodeId, metadata, metadata.tokenMap.tokens(nodeId)); + } + + public void mergeNodeToGossip(NodeId nodeId, ClusterMetadata metadata, Collection tokens) + { + taskLock.lock(); + try + { + boolean isLocal = nodeId.equals(metadata.myNodeId()); + IPartitioner partitioner = metadata.tokenMap.partitioner(); + NodeAddresses addresses = metadata.directory.getNodeAddresses(nodeId); + Location location = metadata.directory.location(nodeId); + InetAddressAndPort endpoint = addresses.broadcastAddress; + + VersionedValue.VersionedValueFactory valueFactory = isLocal ? StorageService.instance.valueFactory : new VersionedValue.VersionedValueFactory(partitioner, () -> 0); + Gossiper.runInGossipStageBlocking(() -> { + EndpointState epstate = getEndpointStateForEndpoint(endpoint); + if (epstate == null) + epstate = new EndpointState(HeartBeatState.empty()); + Map newStates = new EnumMap<>(ApplicationState.class); + for (ApplicationState appState : ApplicationState.values()) + { + VersionedValue oldValue = epstate.getApplicationState(appState); + VersionedValue newValue = null; + switch (appState) + { + case DC: + newValue = valueFactory.datacenter(location.datacenter); + break; + case SCHEMA: + newValue = valueFactory.schema(metadata.schema.getVersion()); + break; + case RACK: + newValue = valueFactory.rack(location.rack); + break; + case RELEASE_VERSION: + newValue = valueFactory.releaseVersion(metadata.directory.version(nodeId).cassandraVersion.toString()); + break; + case RPC_ADDRESS: + newValue = valueFactory.rpcaddress(endpoint.getAddress()); + break; + case HOST_ID: + // If still running in gossip mode, meaning the upgrade to TCM isn't fully complete, + // continue to gossip the old host id value here, not the node id + UUID uuid = ClusterMetadataService.state() == ClusterMetadataService.State.GOSSIP + ? metadata.directory.hostId(nodeId) + : nodeId.toUUID(); + newValue = valueFactory.hostId(uuid); + break; + case TOKENS: + if (tokens != null) + newValue = valueFactory.tokens(tokens); + break; + case INTERNAL_ADDRESS_AND_PORT: + newValue = valueFactory.internalAddressAndPort(addresses.localAddress); + break; + case NATIVE_ADDRESS_AND_PORT: + newValue = valueFactory.nativeaddressAndPort(addresses.nativeAddress); + break; + case STATUS: + // only publish/add STATUS if there are non-upgraded hosts + if (metadata.directory.versions.values().stream().allMatch(NodeVersion::isUpgraded)) + break; + case STATUS_WITH_PORT: + // if StorageService.instance.shouldJoinRing() == false, the node was started with + // -Dcassandra.join_ring=false and an operator is yet to manually join via JMX. + // In this case, the app state will be set to `hibernate` by StorageService, so + // don't set it here as nodeStateToStatus only considers persistent states (e.g. + // ones stored in ClusterMetadata), it isn't aware of transient states like hibernate. + if (isLocal && !StorageService.instance.shouldJoinRing()) + break; + newValue = GossipHelper.nodeStateToStatus(nodeId, metadata, tokens, valueFactory, oldValue); + break; + default: + newValue = oldValue; + } + if (newValue != null) + { + // note that version needs to be > -1 here, otherwise Gossiper#sendAll on generation change doesn't send it + if (!isLocal) + newValue = unsafeMakeVersionedValue(newValue.value, oldValue == null ? 0 : oldValue.version); + newStates.put(appState, newValue); + } + } + HeartBeatState heartBeatState = new HeartBeatState(epstate.getHeartBeatState().getGeneration(), isLocal ? VersionGenerator.getNextVersion() : 0); + EndpointState newepstate = new EndpointState(heartBeatState, newStates); + unsafeUpdateEpStates(endpoint, newepstate); + logger.debug("Updated epstates for {}: {}", endpoint, newepstate); + }); + } + catch (Exception e) + { + logger.warn("Could not merge node {} to gossip", nodeId, e); + } + finally + { + taskLock.unlock(); + } + } + + public void triggerRoundWithCMS() + { + ClusterMetadata metadata = ClusterMetadata.current(); + Set cms = metadata.fullCMSMembers(); + if (!cms.contains(getBroadcastAddressAndPort())) + { + logger.debug("Triggering gossip round with CMS {}", metadata.epoch); + final List gDigests = new ArrayList<>(); + Gossiper.instance.makeGossipDigest(gDigests); + GossipDigestSyn digestSynMessage = new GossipDigestSyn(getClusterName(), + getPartitionerName(), + gDigests); + Message message = Message.out(GOSSIP_DIGEST_SYN, digestSynMessage); + sendGossip(message, cms); + } + } } diff --git a/src/java/org/apache/cassandra/gms/GossiperEvent.java b/src/java/org/apache/cassandra/gms/GossiperEvent.java index 71fee7c991..366c1bbf25 100644 --- a/src/java/org/apache/cassandra/gms/GossiperEvent.java +++ b/src/java/org/apache/cassandra/gms/GossiperEvent.java @@ -19,6 +19,7 @@ package org.apache.cassandra.gms; import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +46,7 @@ public final class GossiperEvent extends DiagnosticEvent private final long lastProcessedMessageAt; private final Set liveEndpoints; private final List seeds; + @Deprecated(since = "CEP-21") private final Set seedsInShadowRound; private final Map unreachableEndpoints; @@ -77,12 +79,13 @@ public final class GossiperEvent extends DiagnosticEvent this.localState = localState; this.endpointStateMap = gossiper.getEndpointStateMap(); - this.inShadowRound = gossiper.isInShadowRound(); + this.inShadowRound = false; // todo; gossiper.isInShadowRound(); this.justRemovedEndpoints = gossiper.getJustRemovedEndpoints(); this.lastProcessedMessageAt = gossiper.getLastProcessedMessageAt(); this.liveEndpoints = gossiper.getLiveMembers(); this.seeds = gossiper.getSeeds(); - this.seedsInShadowRound = gossiper.getSeedsInShadowRound(); + // Implementation of shadow round has changed with CEP-21, so this is no longer relevant but remains for compatibility + this.seedsInShadowRound = Collections.emptySet(); this.unreachableEndpoints = gossiper.getUnreachableEndpoints(); } diff --git a/src/java/org/apache/cassandra/gms/GossiperMBean.java b/src/java/org/apache/cassandra/gms/GossiperMBean.java index 2d59e37f2d..0552883a60 100644 --- a/src/java/org/apache/cassandra/gms/GossiperMBean.java +++ b/src/java/org/apache/cassandra/gms/GossiperMBean.java @@ -27,8 +27,10 @@ public interface GossiperMBean public int getCurrentGenerationNumber(String address) throws UnknownHostException; + @Deprecated(since = "CEP-21") public void unsafeAssassinateEndpoint(String address) throws UnknownHostException; + @Deprecated(since = "CEP-21") public void assassinateEndpoint(String address) throws UnknownHostException; public List reloadSeeds(); diff --git a/src/java/org/apache/cassandra/gms/NewGossiper.java b/src/java/org/apache/cassandra/gms/NewGossiper.java new file mode 100644 index 0000000000..8473a40d34 --- /dev/null +++ b/src/java/org/apache/cassandra/gms/NewGossiper.java @@ -0,0 +1,178 @@ +/* + * 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.gms; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tcm.compatibility.GossipHelper; +import org.apache.cassandra.utils.concurrent.Accumulator; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Promise; + +import static org.apache.cassandra.config.DatabaseDescriptor.getClusterName; +import static org.apache.cassandra.config.DatabaseDescriptor.getPartitionerName; +import static org.apache.cassandra.net.Verb.GOSSIP_DIGEST_SYN; +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; + +public class NewGossiper +{ + private static final Logger logger = LoggerFactory.getLogger(NewGossiper.class); + public static final NewGossiper instance = new NewGossiper(); + + private volatile ShadowRoundHandler handler; + + public Map doShadowRound() + { + Set peers = new HashSet<>(SystemKeyspace.loadHostIds().keySet()); + if (peers.isEmpty()) + peers.addAll(DatabaseDescriptor.getSeeds()); + if (peers.equals(Collections.singleton(getBroadcastAddressAndPort()))) + return GossipHelper.storedEpstate(); + + ShadowRoundHandler shadowRoundHandler = new ShadowRoundHandler(peers); + handler = shadowRoundHandler; + + int tries = 0; + while (true) + { + try + { + return shadowRoundHandler.doShadowRound().get(15, TimeUnit.SECONDS); + } + catch (InterruptedException | ExecutionException | TimeoutException e) + { + if (++tries > 3) + break; + logger.warn("Got no response for shadow round"); + } + } + logger.warn("Not able to construct initial cluster metadata from gossip, using system tables instead"); + return GossipHelper.storedEpstate(); + } + + public boolean isInShadowRound() + { + ShadowRoundHandler srh = handler; + return srh != null && !srh.isDone(); + } + + void onAck( Map epStateMap) + { + ShadowRoundHandler srh = handler; + if (srh != null && !srh.isDone()) + srh.onAck(epStateMap); + } + + public static class ShadowRoundHandler + { + private volatile boolean isDone = false; + private final Set peers; + private final Accumulator> responses; + private final int requiredResponses; + private final MessageDelivery messageDelivery; + private final Promise> promise = new AsyncPromise<>(); + + public ShadowRoundHandler(Set peers) + { + this(peers, MessagingService.instance()); + } + + public ShadowRoundHandler(Set peers, MessageDelivery messageDelivery) + { + this.peers = peers; + requiredResponses = Math.max(peers.size() / 10, 1); // todo: is 10% reasonable? + responses = new Accumulator<>(requiredResponses); + this.messageDelivery = messageDelivery; + } + + public boolean isDone() + { + return isDone; + } + + public Promise> doShadowRound() + { + // send a completely empty syn + GossipDigestSyn digestSynMessage = new GossipDigestSyn(getClusterName(), getPartitionerName(), new ArrayList<>()); + Message message = Message.out(GOSSIP_DIGEST_SYN, digestSynMessage); + + logger.info("Sending shadow round GOSSIP DIGEST SYN to known peers {}", peers); + for (InetAddressAndPort peer : peers) + { + if (!peer.equals(getBroadcastAddressAndPort())) + messageDelivery.send(message, peer); + } + return promise; + } + + public void onAck(Map epStateMap) + { + if (!isDone) + { + if (!epStateMap.isEmpty()) + responses.add(epStateMap); + + logger.debug("Received {} responses. {} required.", responses.size(), requiredResponses); + if (responses.size() >= requiredResponses) + { + isDone = true; + Map merged = merge(responses.snapshot()); + if (GossipHelper.isValidForClusterMetadata(merged)) + promise.setSuccess(merged); + else + promise.setFailure(new IllegalStateException("Did not get all required application states during shadow round")); + } + } + } + + private Map merge(Collection> snapshot) + { + Map mergedStates = new HashMap<>(); + for (Map states : snapshot) + { + for (Map.Entry entry : states.entrySet()) + { + InetAddressAndPort endpoint = entry.getKey(); + EndpointState state = entry.getValue(); + if (!mergedStates.containsKey(entry.getKey()) || mergedStates.get(endpoint).isSupersededBy(state)) + mergedStates.put(endpoint, state); + } + } + return mergedStates; + } + } +} diff --git a/src/java/org/apache/cassandra/gms/VersionedValue.java b/src/java/org/apache/cassandra/gms/VersionedValue.java index 680a0d8093..0c9a0138a0 100644 --- a/src/java/org/apache/cassandra/gms/VersionedValue.java +++ b/src/java/org/apache/cassandra/gms/VersionedValue.java @@ -24,6 +24,7 @@ import java.net.InetAddress; import java.util.Collection; import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; @@ -71,20 +72,31 @@ public class VersionedValue implements Comparable public final static String DELIMITER_STR = new String(new char[]{ DELIMITER }); // values for ApplicationState.STATUS + @Deprecated(since = "CEP-21") public final static String STATUS_BOOTSTRAPPING = "BOOT"; + @Deprecated(since = "CEP-21") public final static String STATUS_BOOTSTRAPPING_REPLACE = "BOOT_REPLACE"; + @Deprecated(since = "CEP-21") public final static String STATUS_NORMAL = "NORMAL"; + @Deprecated(since = "CEP-21") public final static String STATUS_LEAVING = "LEAVING"; + @Deprecated(since = "CEP-21") public final static String STATUS_LEFT = "LEFT"; + @Deprecated(since = "CEP-21") public final static String STATUS_MOVING = "MOVING"; + @Deprecated(since = "CEP-21") public final static String REMOVING_TOKEN = "removing"; + @Deprecated(since = "CEP-21") public final static String REMOVED_TOKEN = "removed"; + @Deprecated(since = "CEP-21") public final static String HIBERNATE = "hibernate"; + @Deprecated(since = "CEP-21") public final static String SHUTDOWN = "shutdown"; // values for ApplicationState.REMOVAL_COORDINATOR + @Deprecated(since = "CEP-21") public final static String REMOVAL_COORDINATOR = "REMOVER"; public static Set BOOTSTRAPPING_STATUS = ImmutableSet.of(STATUS_BOOTSTRAPPING, STATUS_BOOTSTRAPPING_REPLACE); @@ -139,10 +151,17 @@ public class VersionedValue implements Comparable public static class VersionedValueFactory { final IPartitioner partitioner; + private final Supplier versionSupplier; public VersionedValueFactory(IPartitioner partitioner) + { + this(partitioner, VersionGenerator::getNextVersion); + } + + public VersionedValueFactory(IPartitioner partitioner, Supplier versionSupplier) { this.partitioner = partitioner; + this.versionSupplier = versionSupplier; } public VersionedValue cloneWithHigherVersion(VersionedValue value) @@ -154,24 +173,29 @@ public class VersionedValue implements Comparable @Deprecated(since = "4.0") public VersionedValue bootReplacing(InetAddress oldNode) { - return new VersionedValue(versionString(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE, oldNode.getHostAddress())); + return new VersionedValue(versionString(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE, oldNode.getHostAddress()), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue bootReplacingWithPort(InetAddressAndPort oldNode) { - return new VersionedValue(versionString(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE, oldNode.getHostAddressAndPort())); + return new VersionedValue(versionString(VersionedValue.STATUS_BOOTSTRAPPING_REPLACE, oldNode.getHostAddressAndPort()), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue bootstrapping(Collection tokens) { return new VersionedValue(versionString(VersionedValue.STATUS_BOOTSTRAPPING, - makeTokenString(tokens))); + makeTokenString(tokens)), + versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue normal(Collection tokens) { return new VersionedValue(versionString(VersionedValue.STATUS_NORMAL, - makeTokenString(tokens))); + makeTokenString(tokens)), + versionSupplier.get()); } private String makeTokenString(Collection tokens) @@ -181,7 +205,8 @@ public class VersionedValue implements Comparable public VersionedValue load(double load) { - return new VersionedValue(String.valueOf(load)); + return new VersionedValue(String.valueOf(load), + versionSupplier.get()); } public VersionedValue diskUsage(String state) @@ -191,22 +216,27 @@ public class VersionedValue implements Comparable public VersionedValue schema(UUID newVersion) { - return new VersionedValue(newVersion.toString()); + return new VersionedValue(newVersion.toString(), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue leaving(Collection tokens) { return new VersionedValue(versionString(VersionedValue.STATUS_LEAVING, - makeTokenString(tokens))); + makeTokenString(tokens)), + versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue left(Collection tokens, long expireTime) { return new VersionedValue(versionString(VersionedValue.STATUS_LEFT, makeTokenString(tokens), - Long.toString(expireTime))); + Long.toString(expireTime)), + versionSupplier.get()); } + @Deprecated(since = "CEP-21") @VisibleForTesting public VersionedValue left(Collection tokens, long expireTime, int generation) { @@ -215,16 +245,20 @@ public class VersionedValue implements Comparable Long.toString(expireTime)), generation); } + @Deprecated(since = "CEP-21") public VersionedValue moving(Token token) { - return new VersionedValue(VersionedValue.STATUS_MOVING + VersionedValue.DELIMITER + partitioner.getTokenFactory().toString(token)); + return new VersionedValue(VersionedValue.STATUS_MOVING + VersionedValue.DELIMITER + partitioner.getTokenFactory().toString(token), + versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue hostId(UUID hostId) { - return new VersionedValue(hostId.toString()); + return new VersionedValue(hostId.toString(), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue tokens(Collection tokens) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -237,37 +271,40 @@ public class VersionedValue implements Comparable { throw new RuntimeException(e); } - return new VersionedValue(new String(bos.toByteArray(), ISO_8859_1)); + return new VersionedValue(new String(bos.toByteArray(), ISO_8859_1), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue removingNonlocal(UUID hostId) { - return new VersionedValue(versionString(VersionedValue.REMOVING_TOKEN, hostId.toString())); + return new VersionedValue(versionString(VersionedValue.REMOVING_TOKEN, hostId.toString()), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue removedNonlocal(UUID hostId, long expireTime) { - return new VersionedValue(versionString(VersionedValue.REMOVED_TOKEN, hostId.toString(), Long.toString(expireTime))); + return new VersionedValue(versionString(VersionedValue.REMOVED_TOKEN, hostId.toString(), Long.toString(expireTime)), versionSupplier.get()); } + @Deprecated(since = "CEP-21") public VersionedValue removalCoordinator(UUID hostId) { - return new VersionedValue(versionString(VersionedValue.REMOVAL_COORDINATOR, hostId.toString())); + return new VersionedValue(versionString(VersionedValue.REMOVAL_COORDINATOR, hostId.toString()), versionSupplier.get()); } public VersionedValue hibernate(boolean value) { - return new VersionedValue(VersionedValue.HIBERNATE + VersionedValue.DELIMITER + value); + return new VersionedValue(VersionedValue.HIBERNATE + VersionedValue.DELIMITER + value, versionSupplier.get()); } public VersionedValue rpcReady(boolean value) { - return new VersionedValue(String.valueOf(value)); + return new VersionedValue(String.valueOf(value), versionSupplier.get()); } public VersionedValue shutdown(boolean value) { - return new VersionedValue(VersionedValue.SHUTDOWN + VersionedValue.DELIMITER + value); + return new VersionedValue(VersionedValue.SHUTDOWN + VersionedValue.DELIMITER + value, versionSupplier.get()); } public VersionedValue indexStatus(String status) @@ -277,59 +314,59 @@ public class VersionedValue implements Comparable public VersionedValue datacenter(String dcId) { - return new VersionedValue(dcId); + return new VersionedValue(dcId, versionSupplier.get()); } public VersionedValue rack(String rackId) { - return new VersionedValue(rackId); + return new VersionedValue(rackId, versionSupplier.get()); } public VersionedValue rpcaddress(InetAddress endpoint) { - return new VersionedValue(endpoint.getHostAddress()); + return new VersionedValue(endpoint.getHostAddress(), versionSupplier.get()); } public VersionedValue nativeaddressAndPort(InetAddressAndPort address) { - return new VersionedValue(address.getHostAddressAndPort()); + return new VersionedValue(address.getHostAddressAndPort(), versionSupplier.get()); } public VersionedValue releaseVersion() { - return new VersionedValue(FBUtilities.getReleaseVersionString()); + return new VersionedValue(FBUtilities.getReleaseVersionString(), versionSupplier.get()); } @VisibleForTesting public VersionedValue releaseVersion(String version) { - return new VersionedValue(version); + return new VersionedValue(version, versionSupplier.get()); } @VisibleForTesting public VersionedValue networkVersion(int version) { - return new VersionedValue(String.valueOf(version)); + return new VersionedValue(String.valueOf(version), versionSupplier.get()); } public VersionedValue networkVersion() { - return new VersionedValue(String.valueOf(MessagingService.current_version)); + return new VersionedValue(String.valueOf(MessagingService.current_version), versionSupplier.get()); } public VersionedValue internalIP(InetAddress private_ip) { - return new VersionedValue(private_ip.getHostAddress()); + return new VersionedValue(private_ip.getHostAddress(), versionSupplier.get()); } public VersionedValue internalAddressAndPort(InetAddressAndPort private_ip_and_port) { - return new VersionedValue(private_ip_and_port.getHostAddressAndPort()); + return new VersionedValue(private_ip_and_port.getHostAddressAndPort(), versionSupplier.get()); } public VersionedValue severity(double value) { - return new VersionedValue(String.valueOf(value)); + return new VersionedValue(String.valueOf(value), versionSupplier.get()); } public VersionedValue sstableVersions(Set versions) diff --git a/src/java/org/apache/cassandra/hints/HintVerbHandler.java b/src/java/org/apache/cassandra/hints/HintVerbHandler.java index e6758d0f75..8b9846dc59 100644 --- a/src/java/org/apache/cassandra/hints/HintVerbHandler.java +++ b/src/java/org/apache/cassandra/hints/HintVerbHandler.java @@ -31,6 +31,8 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; /** * Verb handler used both for hint dispatch and streaming. @@ -77,10 +79,14 @@ public final class HintVerbHandler implements IVerbHandler return; } - if (!hostId.equals(StorageService.instance.getLocalHostUUID())) + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId localId = metadata.myNodeId(); + if (!hostId.equals(localId.toUUID()) && !hostId.equals(metadata.directory.hostId(localId))) { - // the node is not the final destination of the hint (must have gotten it from a decommissioning node), - // so just store it locally, to be delivered later. + // the hint may have been written prior to upgrading, in which case it would be addressing the old + // host id for its target node. If the id in the hint matches neither the pre-upgrade host id nor the + // post-upgrade node id for this peer, the node is not the final destination of the hint (must have gotten + // it from a decommissioning node), so just store it locally, to be delivered later. HintsService.instance.write(hostId, hint); respond(message); } diff --git a/src/java/org/apache/cassandra/hints/HintsCatalog.java b/src/java/org/apache/cassandra/hints/HintsCatalog.java index 6bc0030924..e989850dff 100644 --- a/src/java/org/apache/cassandra/hints/HintsCatalog.java +++ b/src/java/org/apache/cassandra/hints/HintsCatalog.java @@ -162,6 +162,10 @@ final class HintsCatalog FileUtils.handleFSErrorAndPropagate(e); } } + else if (!NativeLibrary.isEnabled()) + { + return; + } else if (DatabaseDescriptor.isClientInitialized()) { logger.warn("Unable to open hint directory using Native library. Skipping sync."); diff --git a/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java b/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java index 540f5bd85d..3e4e8bc618 100644 --- a/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java +++ b/src/java/org/apache/cassandra/hints/HintsDispatchExecutor.java @@ -38,6 +38,7 @@ import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import org.apache.cassandra.utils.concurrent.Future; @@ -113,7 +114,7 @@ final class HintsDispatchExecutor return scheduledDispatches.computeIfAbsent(hostId, uuid -> executor.submit(new DispatchHintsTask(store, hostId))); } - Future transfer(HintsCatalog catalog, Supplier hostIdSupplier) + Future transfer(HintsCatalog catalog, Supplier hostIdSupplier) { return executor.submit(new TransferHintsTask(catalog, hostIdSupplier)); } @@ -212,11 +213,11 @@ final class HintsDispatchExecutor // Rate limit is in bytes per second. Uses Double.MAX_VALUE if disabled (set to 0 in cassandra.yaml). // Max rate is scaled by the number of nodes in the cluster (CASSANDRA-5272), unless we are transferring - // hints during decomission rather than dispatching them to their final destination. + // hints during decommission rather than dispatching them to their final destination. // The goal is to bound maximum hints traffic going towards a particular node from the rest of the cluster, // not total outgoing hints traffic from this node. This is why the rate limiter is not shared between // all the dispatch tasks (as there will be at most one dispatch task for a particular host id at a time). - int nodesCount = isTransfer ? 1 : Math.max(1, StorageService.instance.getTokenMetadata().getAllEndpoints().size() - 1); + int nodesCount = isTransfer ? 1 : Math.max(1, ClusterMetadata.current().directory.allAddresses().size() - 1); double throttleInBytes = DatabaseDescriptor.getHintedHandoffThrottleInKiB() * 1024.0 / nodesCount; this.rateLimiter = RateLimiter.create(throttleInBytes == 0 ? Double.MAX_VALUE : throttleInBytes); } diff --git a/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java b/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java index 0dfc6e1323..488f9abed7 100644 --- a/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java +++ b/src/java/org/apache/cassandra/hints/HintsDispatchTrigger.java @@ -62,7 +62,7 @@ final class HintsDispatchTrigger implements Runnable .filter(store -> !isScheduled(store)) .filter(HintsStore::isLive) .filter(store -> store.isWriting() || store.hasFiles()) - .filter(store -> Schema.instance.isSameVersion(Gossiper.instance.getSchemaVersion(store.address()))) + .filter(store -> Schema.instance.getVersion().equals(Gossiper.instance.getSchemaVersion(store.address()))) .forEach(this::schedule); } diff --git a/src/java/org/apache/cassandra/hints/HintsReader.java b/src/java/org/apache/cassandra/hints/HintsReader.java index fc6796b624..2738f023f7 100644 --- a/src/java/org/apache/cassandra/hints/HintsReader.java +++ b/src/java/org/apache/cassandra/hints/HintsReader.java @@ -27,12 +27,14 @@ import javax.annotation.Nullable; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.RateLimiter; +import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.io.util.File; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.FSReadError; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.AbstractIterator; @@ -239,12 +241,13 @@ class HintsReader implements AutoCloseable, Iterable hint = Hint.serializer.deserializeIfLive(input, now, size, descriptor.messagingVersion()); input.checkLimit(0); } - catch (UnknownTableException e) + catch (UnknownTableException | CoordinatorBehindException e) { + TableId id = ((UnknownTableException) (e instanceof CoordinatorBehindException ? e.getCause() : e)).id; logger.warn("Failed to read a hint for {}: {} - table with id {} is unknown in file {}", StorageService.instance.getEndpointForHostId(descriptor.hostId), descriptor.hostId, - e.id, + id, descriptor.fileName()); input.skipBytes(Ints.checkedCast(size - input.bytesPastLimit())); diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java index 9fd24bcec3..8954e20a8e 100644 --- a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java +++ b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java @@ -75,6 +75,7 @@ import org.apache.cassandra.notifications.SSTableAddedNotification; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.Indexes; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.pager.SinglePartitionPager; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.ProtocolVersion; @@ -193,10 +194,10 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum /** * Drops and adds new indexes associated with the underlying CF */ - public void reload() + public void reload(TableMetadata baseTable) { // figure out what needs to be added and dropped. - Indexes tableIndexes = baseCfs.metadata().indexes; + Indexes tableIndexes = baseTable.indexes; indexes.keySet() .stream() .filter(indexName -> !tableIndexes.has(indexName)) @@ -205,7 +206,7 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum // we call add for every index definition in the collection as // some may not have been created here yet, only added to schema for (IndexMetadata tableIndex : tableIndexes) - addIndex(tableIndex, false); + addIndex(tableIndex, SystemKeyspace.isIndexBuilt(baseTable.keyspace, tableIndex.name)); } private Future reloadIndex(IndexMetadata indexDef) diff --git a/src/java/org/apache/cassandra/index/TargetParser.java b/src/java/org/apache/cassandra/index/TargetParser.java index 9ada4c640b..e13bc17454 100644 --- a/src/java/org/apache/cassandra/index/TargetParser.java +++ b/src/java/org/apache/cassandra/index/TargetParser.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.index; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +37,13 @@ public class TargetParser private static final Pattern TWO_QUOTES = Pattern.compile("\"\""); private static final String QUOTE = "\""; + public static Optional> tryParse(TableMetadata metadata, IndexMetadata indexDef) + { + String target = indexDef.options.get("target"); + assert target != null : String.format("No target definition found for index %s", indexDef.name); + return Optional.ofNullable(parse(metadata, target)); + } + public static Pair parse(TableMetadata metadata, IndexMetadata indexDef) { String target = indexDef.options.get("target"); diff --git a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java index 125c6e9dbd..efa79d6956 100644 --- a/src/java/org/apache/cassandra/index/internal/CassandraIndex.java +++ b/src/java/org/apache/cassandra/index/internal/CassandraIndex.java @@ -33,8 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.statements.schema.IndexTarget; @@ -56,7 +56,6 @@ import org.apache.cassandra.index.internal.keys.KeysSearcher; import org.apache.cassandra.index.transactions.IndexTransaction; import org.apache.cassandra.io.sstable.ReducingKeyIterator; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Refs; @@ -201,7 +200,7 @@ public abstract class CassandraIndex implements Index public Callable getMetadataReloadTask(IndexMetadata indexDef) { return () -> { - indexCfs.reload(); + indexCfs.reload(indexCfs.metadata()); return null; }; } @@ -224,10 +223,10 @@ public abstract class CassandraIndex implements Index metadata = indexDef; Pair target = TargetParser.parse(baseCfs.metadata(), indexDef); functions = getFunctions(indexDef, target); - TableMetadataRef tableRef = TableMetadataRef.forOfflineTools(indexCfsMetadata(baseCfs.metadata(), indexDef)); + TableMetadata tm = indexCfsMetadata(baseCfs.metadata(), indexDef); indexCfs = ColumnFamilyStore.createColumnFamilyStore(baseCfs.keyspace, - tableRef.name, - tableRef, + tm.name, + tm, baseCfs.getTracker().loadsstables); indexedColumn = target.left; } diff --git a/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java index 7430ce3412..d0d6ac2f68 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/ColumnIndexesSystemView.java @@ -32,6 +32,7 @@ import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; @@ -79,11 +80,11 @@ public class ColumnIndexesSystemView extends AbstractVirtualTable { SimpleDataSet dataset = new SimpleDataSet(metadata()); - for (String ks : Schema.instance.getUserKeyspaces()) + for (KeyspaceMetadata ks: Schema.instance.getUserKeyspaces()) { - Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); + Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks.name); if (keyspace == null) - throw new IllegalArgumentException("Unknown keyspace " + ks); + throw new IllegalArgumentException("Unknown keyspace " + ks.name); for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) { @@ -97,7 +98,7 @@ public class ColumnIndexesSystemView extends AbstractVirtualTable IndexContext context = ((StorageAttachedIndex) index).getIndexContext(); String indexName = context.getIndexName(); - dataset.row(ks, indexName) + dataset.row(ks.name, indexName) .column(TABLE_NAME, cfs.name) .column(COLUMN_NAME, context.getColumnName()) .column(IS_QUERYABLE, manager.isIndexQueryable(index)) diff --git a/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java index 4d72d01db1..4692fba878 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/SSTableIndexesSystemView.java @@ -34,6 +34,7 @@ import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; import org.apache.cassandra.index.sai.disk.SSTableIndex; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; @@ -85,9 +86,9 @@ public class SSTableIndexesSystemView extends AbstractVirtualTable { SimpleDataSet dataset = new SimpleDataSet(metadata()); - for (String ks : Schema.instance.getUserKeyspaces()) + for (KeyspaceMetadata ks : Schema.instance.getUserKeyspaces()) { - Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); + Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks.name); if (keyspace == null) throw new IllegalStateException("Unknown keyspace " + ks + ". This can occur if the keyspace is being dropped."); @@ -109,7 +110,7 @@ public class SSTableIndexesSystemView extends AbstractVirtualTable Descriptor descriptor = sstable.descriptor; AbstractBounds bounds = sstable.getBounds(); - dataset.row(ks, indexContext.getIndexName(), sstable.getFilename()) + dataset.row(ks.name, indexContext.getIndexName(), sstable.getFilename()) .column(TABLE_NAME, descriptor.cfname) .column(COLUMN_NAME, indexContext.getColumnName()) .column(FORMAT_VERSION, sstableIndex.getVersion().toString()) diff --git a/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java b/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java index 3829ef305e..b4ee2a8972 100644 --- a/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java +++ b/src/java/org/apache/cassandra/index/sai/virtual/SegmentsSystemView.java @@ -33,6 +33,7 @@ import org.apache.cassandra.index.sai.IndexContext; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; import org.apache.cassandra.index.sai.disk.SSTableIndex; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; @@ -101,9 +102,9 @@ public class SegmentsSystemView extends AbstractVirtualTable private void forEachIndex(Consumer process) { - for (String ks : Schema.instance.getUserKeyspaces()) + for (KeyspaceMetadata ks : Schema.instance.getUserKeyspaces()) { - Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks); + Keyspace keyspace = Schema.instance.getKeyspaceInstance(ks.name); if (keyspace == null) throw new IllegalStateException("Unknown keyspace " + ks + ". This can occur if the keyspace is being dropped."); diff --git a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java index 3b83314fbc..0d804bf4f2 100644 --- a/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/CQLSSTableWriter.java @@ -65,6 +65,7 @@ import org.apache.cassandra.schema.Types; import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.schema.Views; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.JavaDriverUtils; @@ -120,6 +121,7 @@ public class CQLSSTableWriter implements Closeable // Partitioner is not set in client mode. if (DatabaseDescriptor.getPartitioner() == null) DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ClusterMetadataService.initializeForClients(); } private final AbstractSSTableSimpleWriter writer; @@ -568,30 +570,34 @@ public class CQLSSTableWriter implements Closeable CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.getKey()); synchronized (CQLSSTableWriter.class) { - String keyspaceName = schemaStatement.keyspace(); - Schema.instance.transform(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(keyspaceName, - KeyspaceParams.simple(1), - Tables.none(), - Views.none(), - Types.none(), - UserFunctions.none()), true)); + Schema.instance.submit(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(keyspaceName, + KeyspaceParams.simple(1), + Tables.none(), + Views.none(), + Types.none(), + UserFunctions.none()), true)); - KeyspaceMetadata ksm = Schema.instance.getKeyspaceMetadata(keyspaceName); + KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspaceName, + KeyspaceParams.simple(1), + Tables.none(), + Views.none(), + Types.none(), + UserFunctions.none()); TableMetadata tableMetadata = ksm.tables.getNullable(schemaStatement.table()); if (tableMetadata == null) { Types types = createTypes(keyspaceName); - Schema.instance.transform(SchemaTransformations.addTypes(types, true)); + Schema.instance.submit(SchemaTransformations.addTypes(types, true)); tableMetadata = createTable(types); - Schema.instance.transform(SchemaTransformations.addTable(tableMetadata, true)); + Schema.instance.submit(SchemaTransformations.addTable(tableMetadata, true)); } ModificationStatement preparedModificationStatement = prepareModificationStatement(); - TableMetadataRef ref = TableMetadataRef.forOfflineTools(tableMetadata); + TableMetadataRef ref = tableMetadata.ref; AbstractSSTableSimpleWriter writer = sorted ? new SSTableSimpleWriter(directory, ref, preparedModificationStatement.updatedColumns()) : new SSTableSimpleUnsortedWriter(directory, ref, preparedModificationStatement.updatedColumns(), bufferSizeInMiB); diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java index fd1b6a0a81..92643b21b8 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleIterator.java @@ -21,6 +21,8 @@ import java.io.IOException; import java.io.IOError; import java.util.Iterator; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.db.*; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.io.util.DataInputPlus; @@ -125,4 +127,28 @@ public abstract class SSTableSimpleIterator extends AbstractIterator } } } + + @VisibleForTesting + public static class EmptySSTableSimpleIterator extends SSTableSimpleIterator + { + public EmptySSTableSimpleIterator(TableMetadata metadata) + { + super(metadata, null, null); + } + + public Row readStaticRow() throws IOException + { + return Rows.EMPTY_STATIC_ROW; + } + + protected Unfiltered computeNext() + { + return null; + } + + public boolean hasNext() + { + return false; + } + } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java index 4f622be5a5..9c2fd1b488 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java @@ -345,7 +345,7 @@ public abstract class SSTableReader extends SSTable implements UnfilteredSource, public static SSTableReader open(SSTable.Owner owner, Descriptor desc, TableMetadataRef metadata) { - return open(owner, desc, null, metadata); + return open(owner, desc, null, metadata); } public static SSTableReader open(SSTable.Owner owner, Descriptor descriptor, Set components, TableMetadataRef metadata) @@ -393,7 +393,6 @@ public abstract class SSTableReader extends SSTable implements UnfilteredSource, boolean isOffline) { SSTableReaderLoadingBuilder builder = descriptor.getFormat().getReaderFactory().loadingBuilder(descriptor, metadata, components); - return builder.build(owner, validate, !isOffline); } @@ -535,7 +534,7 @@ public abstract class SSTableReader extends SSTable implements UnfilteredSource, public void setupOnline() { - owner().ifPresent(o -> setCrcCheckChance(o.getCrcCheckChance())); + owner().ifPresent(o -> setCrcCheckChance(o.getCrcCheckChance())); } /** diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java index aedd860922..1a4aa0d9f5 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReaderLoadingBuilder.java @@ -141,7 +141,7 @@ public abstract class SSTableReaderLoadingBuilder m.ref).orElse(null); if (metadata == null) throw new AssertionError("Could not find index metadata for index cf " + i); } diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java index bf8bb79ce2..3e469a8932 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java @@ -164,7 +164,6 @@ public class BigTableWriter extends SortedTableWriter map) + { + Replica[] contents = new Replica[size]; + for (int i = 0; i < contents.length; i++) + { + if (this.contents[i] != null) + contents[i] = map.apply(this.contents[i]); + } + + return new ReplicaList(contents, begin, contents.length); + } + public void add(Replica replica) { // can only add to full array - if we have sliced it, we must be a snapshot @@ -591,33 +604,30 @@ public abstract class AbstractReplicaCollection - * It's not clear whether {@link AbstractReplicaCollection} should implement the order sensitive {@link Object#equals(Object) equals} - * of {@link java.util.List} or the order oblivious {@link Object#equals(Object) equals} of {@link java.util.Set}. We never rely on equality - * in the database so rather then leave in a potentially surprising implementation we have it throw {@link UnsupportedOperationException}. - *

    - *

    - * Don't implement this and pick one behavior over the other. If you want equality you can static import {@link com.google.common.collect.Iterables#elementsEqual(Iterable, Iterable)} - * and use that to get order sensitive equals. + * Implements order sensitive {@link Object#equals(Object)} #equals() equals} of {@link java.util.List}. *

    */ public final boolean equals(Object o) { - throw new UnsupportedOperationException("AbstractReplicaCollection equals unsupported"); + if (!(o instanceof AbstractReplicaCollection)) + return false; + + return Iterators.elementsEqual(iterator(), ((AbstractReplicaCollection) o).iterator()); } /** *

    - * It's not clear whether {@link AbstractReplicaCollection} should implement the order sensitive {@link Object#hashCode() hashCode} - * of {@link java.util.List} or the order oblivious {@link Object#hashCode() equals} of {@link java.util.Set}. We never rely on hashCode - * in the database so rather then leave in a potentially surprising implementation we have it throw {@link UnsupportedOperationException}. - *

    - *

    - * Don't implement this and pick one behavior over the other. + * Implements order sensitive {@link Object#hashCode() hashCode} of {@link java.util.List}. *

    */ public final int hashCode() { - throw new UnsupportedOperationException("AbstractReplicaCollection hashCode unsupported"); + int result = 1; + + for (Replica e : this) + result = 31 * result + (e == null ? 0 : e.hashCode()); + + return result; } @Override diff --git a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java index 2debc1adf9..b0c5bec7a1 100644 --- a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java +++ b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java @@ -22,98 +22,47 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import java.util.*; import com.google.common.base.Preconditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.WriteType; import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.RingPosition; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; +import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.service.AbstractWriteResponseHandler; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.DatacenterSyncWriteResponseHandler; import org.apache.cassandra.service.DatacenterWriteResponseHandler; import org.apache.cassandra.service.WriteResponseHandler; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; +import org.apache.cassandra.tcm.ownership.DataPlacement; import org.apache.cassandra.utils.FBUtilities; -import org.cliffc.high_scale_lib.NonBlockingHashMap; /** * A abstract parent for all replication strategies. */ public abstract class AbstractReplicationStrategy { - private static final Logger logger = LoggerFactory.getLogger(AbstractReplicationStrategy.class); - public final Map configOptions; + // TODO: remove keyspace name; add a cache that allows going between replication params and replication strategy protected final String keyspaceName; - private final TokenMetadata tokenMetadata; - private final ReplicaCache replicas = new ReplicaCache<>(); - public IEndpointSnitch snitch; - protected AbstractReplicationStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map configOptions) + protected AbstractReplicationStrategy(String keyspaceName, Map configOptions) { - assert snitch != null; - assert tokenMetadata != null; - this.tokenMetadata = tokenMetadata; - this.snitch = snitch; this.configOptions = configOptions == null ? Collections.emptyMap() : configOptions; this.keyspaceName = keyspaceName; } - public EndpointsForRange getCachedReplicas(long ringVersion, Token t) - { - return replicas.get(ringVersion, t); - } - - /** - * get the (possibly cached) endpoints that should store the given Token. - * Note that while the endpoints are conceptually a Set (no duplicates will be included), - * we return a List to avoid an extra allocation when sorting by proximity later - * @param searchPosition the position the natural endpoints are requested for - * @return a copy of the natural endpoints for the given token - */ - public EndpointsForToken getNaturalReplicasForToken(RingPosition searchPosition) - { - return getNaturalReplicas(searchPosition).forToken(searchPosition.getToken()); - } - - public EndpointsForRange getNaturalReplicas(RingPosition searchPosition) - { - Token searchToken = searchPosition.getToken(); - long currentRingVersion = tokenMetadata.getRingVersion(); - Token keyToken = TokenMetadata.firstToken(tokenMetadata.sortedTokens(), searchToken); - EndpointsForRange endpoints = getCachedReplicas(currentRingVersion, keyToken); - if (endpoints == null) - { - TokenMetadata tm = tokenMetadata.cachedOnlyTokenMap(); - // if our cache got invalidated, it's possible there is a new token to account for too - keyToken = TokenMetadata.firstToken(tm.sortedTokens(), searchToken); - endpoints = calculateNaturalReplicas(searchToken, tm); - replicas.put(tm.getRingVersion(), keyToken, endpoints); - } - - return endpoints; - } - - public Replica getLocalReplicaFor(RingPosition searchPosition) - { - return getNaturalReplicas(searchPosition) - .byEndpoint() - .get(FBUtilities.getBroadcastAddressAndPort()); - } - /** * Calculate the natural endpoints for the given token. Endpoints are returned in the order * they occur in the ring following the searchToken, as defined by the replication strategy. @@ -123,14 +72,14 @@ public abstract class AbstractReplicationStrategy * {@link org.apache.cassandra.service.StorageService#getPrimaryRangesForEndpoint(String, InetAddressAndPort)} * which is in turn relied on by various components like repair and size estimate calculations. * - * @see #getNaturalReplicasForToken(org.apache.cassandra.dht.RingPosition) - * - * @param tokenMetadata the token metadata used to find the searchToken, e.g. contains token to endpoint + * @param metadata the token metadata used to find the searchToken, e.g. contains token to endpoint * mapping information * @param searchToken the token to find the natural endpoints for * @return a copy of the natural endpoints for the given token */ - public abstract EndpointsForRange calculateNaturalReplicas(Token searchToken, TokenMetadata tokenMetadata); + public abstract EndpointsForRange calculateNaturalReplicas(Token searchToken, ClusterMetadata metadata); + + public abstract DataPlacement calculateDataPlacement(Epoch epoch, List> ranges, ClusterMetadata metadata); public AbstractWriteResponseHandler getWriteResponseHandler(ReplicaPlan.ForWrite replicaPlan, Runnable callback, @@ -202,90 +151,74 @@ public abstract class AbstractReplicationStrategy { return getReplicationFactor().hasTransientReplicas(); } - /* * NOTE: this is pretty inefficient. also the inverse (getRangeAddresses) below. * this is fine as long as we don't use this on any critical path. * (fixing this would probably require merging tokenmetadata into replicationstrategy, * so we could cache/invalidate cleanly.) */ - public RangesByEndpoint getAddressReplicas(TokenMetadata metadata) + public RangesByEndpoint getAddressReplicas(ClusterMetadata metadata) { RangesByEndpoint.Builder map = new RangesByEndpoint.Builder(); - - for (Token token : metadata.sortedTokens()) + List tokens = metadata.tokenMap.tokens(); + for (Token token : tokens) { - Range range = metadata.getPrimaryRangesFor(List.of(token)).iterator().next(); - for (Replica replica : calculateNaturalReplicas(token, metadata)) + for (Range range : TokenRingUtils.getPrimaryRangesFor(tokens, Collections.singleton(token))) { - // LocalStrategy always returns (min, min] ranges for it's replicas, so we skip the check here - Preconditions.checkState(range.equals(replica.range()) || this instanceof LocalStrategy); - map.put(replica.endpoint(), replica); + for (Replica replica : calculateNaturalReplicas(token, metadata)) + { + // SystemStrategy always returns (min, min] ranges for it's replicas, so we skip the check here + Preconditions.checkState(range.equals(replica.range()) || this instanceof SystemStrategy); + map.put(replica.endpoint(), replica); + } } } return map.build(); } - public RangesAtEndpoint getAddressReplicas(TokenMetadata metadata, InetAddressAndPort endpoint) + public RangesAtEndpoint getAddressReplicas(ClusterMetadata metadata, InetAddressAndPort endpoint) { RangesAtEndpoint.Builder builder = RangesAtEndpoint.builder(endpoint); - for (Token token : metadata.sortedTokens()) + List tokens = metadata.tokenMap.tokens(); + for (Token token : tokens) { - Range range = metadata.getPrimaryRangesFor(List.of(token)).iterator().next(); - Replica replica = calculateNaturalReplicas(token, metadata) - .byEndpoint().get(endpoint); - if (replica != null) + for (Range range : TokenRingUtils.getPrimaryRangesFor(tokens, Collections.singleton(token))) { - // LocalStrategy always returns (min, min] ranges for it's replicas, so we skip the check here - Preconditions.checkState(range.equals(replica.range()) || this instanceof LocalStrategy); - builder.add(replica, Conflict.DUPLICATE); + Replica replica = calculateNaturalReplicas(token, metadata) + .byEndpoint().get(endpoint); + if (replica != null) + { + // SystemStrategy always returns (min, min] ranges for it's replicas, so we skip the check here + Preconditions.checkState(range.equals(replica.range()) || this instanceof SystemStrategy); + builder.add(replica, Conflict.DUPLICATE); + } } } return builder.build(); } - public EndpointsByRange getRangeAddresses(TokenMetadata metadata) + public EndpointsByRange getRangeAddresses(ClusterMetadata metadata) { EndpointsByRange.Builder map = new EndpointsByRange.Builder(); - - for (Token token : metadata.sortedTokens()) + List tokens = metadata.tokenMap.tokens(); + for (Token token : tokens) { - Range range = metadata.getPrimaryRangesFor(List.of(token)).iterator().next(); - for (Replica replica : calculateNaturalReplicas(token, metadata)) + for (Range range : TokenRingUtils.getPrimaryRangesFor(tokens, Collections.singleton(token))) { - // LocalStrategy always returns (min, min] ranges for it's replicas, so we skip the check here - Preconditions.checkState(range.equals(replica.range()) || this instanceof LocalStrategy); - map.put(range, replica); + for (Replica replica : calculateNaturalReplicas(token, metadata)) + { + // SystemStrategy always returns (min, min] ranges for it's replicas, so we skip the check here + Preconditions.checkState(range.equals(replica.range()) || this instanceof SystemStrategy); + map.put(range, replica); + } } } return map.build(); } - public RangesByEndpoint getAddressReplicas() - { - return getAddressReplicas(tokenMetadata.cloneOnlyTokenMap()); - } - - public RangesAtEndpoint getAddressReplicas(InetAddressAndPort endpoint) - { - return getAddressReplicas(tokenMetadata.cloneOnlyTokenMap(), endpoint); - } - - public RangesAtEndpoint getPendingAddressRanges(TokenMetadata metadata, Token pendingToken, InetAddressAndPort pendingAddress) - { - return getPendingAddressRanges(metadata, Collections.singleton(pendingToken), pendingAddress); - } - - public RangesAtEndpoint getPendingAddressRanges(TokenMetadata metadata, Collection pendingTokens, InetAddressAndPort pendingAddress) - { - TokenMetadata temp = metadata.cloneOnlyTokenMap(); - temp.updateNormalTokens(pendingTokens, pendingAddress); - return getAddressReplicas(temp, pendingAddress); - } - public abstract void validateOptions() throws ConfigurationException; /** @deprecated See CASSANDRA-17212 */ @@ -306,7 +239,7 @@ public abstract class AbstractReplicationStrategy * The empty collection means that no options are accepted, but null means * that any option is accepted. */ - public Collection recognizedOptions() + public Collection recognizedOptions(ClusterMetadata metadata) { // We default to null for backward compatibility sake return null; @@ -314,17 +247,15 @@ public abstract class AbstractReplicationStrategy private static AbstractReplicationStrategy createInternal(String keyspaceName, Class strategyClass, - TokenMetadata tokenMetadata, - IEndpointSnitch snitch, Map strategyOptions) throws ConfigurationException { AbstractReplicationStrategy strategy; - Class[] parameterTypes = new Class[] {String.class, TokenMetadata.class, IEndpointSnitch.class, Map.class}; + Class[] parameterTypes = new Class[] {String.class, Map.class}; try { Constructor constructor = strategyClass.getConstructor(parameterTypes); - strategy = constructor.newInstance(keyspaceName, tokenMetadata, snitch, strategyOptions); + strategy = constructor.newInstance(keyspaceName, strategyOptions); } catch (InvocationTargetException e) { @@ -338,24 +269,16 @@ public abstract class AbstractReplicationStrategy return strategy; } + public static AbstractReplicationStrategy createReplicationStrategy(String keyspaceName, + ReplicationParams replicationParams) + { + return createReplicationStrategy(keyspaceName, replicationParams.klass, replicationParams.options); + } public static AbstractReplicationStrategy createReplicationStrategy(String keyspaceName, Class strategyClass, - TokenMetadata tokenMetadata, - IEndpointSnitch snitch, Map strategyOptions) { - AbstractReplicationStrategy strategy = createInternal(keyspaceName, strategyClass, tokenMetadata, snitch, strategyOptions); - - // Because we used to not properly validate unrecognized options, we only log a warning if we find one. - try - { - strategy.validateExpectedOptions(); - } - catch (ConfigurationException e) - { - logger.warn("Ignoring {}", e.getMessage()); - } - + AbstractReplicationStrategy strategy = createInternal(keyspaceName, strategyClass, strategyOptions); strategy.validateOptions(); return strategy; } @@ -395,13 +318,12 @@ public abstract class AbstractReplicationStrategy public static void validateReplicationStrategy(String keyspaceName, Class strategyClass, - TokenMetadata tokenMetadata, - IEndpointSnitch snitch, + ClusterMetadata metadata, Map strategyOptions, ClientState state) throws ConfigurationException { - AbstractReplicationStrategy strategy = createInternal(keyspaceName, strategyClass, tokenMetadata, snitch, strategyOptions); - strategy.validateExpectedOptions(); + AbstractReplicationStrategy strategy = createInternal(keyspaceName, strategyClass, strategyOptions); + strategy.validateExpectedOptions(metadata); strategy.validateOptions(); strategy.maybeWarnOnOptions(state); if (strategy.hasTransientReplicas() && !DatabaseDescriptor.isTransientReplicationEnabled()) @@ -448,76 +370,27 @@ public abstract class AbstractReplicationStrategy } } - protected void validateExpectedOptions() throws ConfigurationException + public void validate(ClusterMetadata snapshot) throws ConfigurationException { - Collection expectedOptions = recognizedOptions(); + validateExpectedOptions(snapshot); + validateOptions(); + maybeWarnOnOptions(); + if (hasTransientReplicas() && !DatabaseDescriptor.isTransientReplicationEnabled()) + { + throw new ConfigurationException("Transient replication is disabled. Enable in cassandra.yaml to use."); + } + } + + public void validateExpectedOptions(ClusterMetadata snapshot) throws ConfigurationException + { + Collection expectedOptions = recognizedOptions(snapshot); if (expectedOptions == null) return; for (String key : configOptions.keySet()) { if (!expectedOptions.contains(key)) - throw new ConfigurationException(String.format("Unrecognized strategy option {%s} passed to %s for keyspace %s", key, getClass().getSimpleName(), keyspaceName)); - } - } - - static class ReplicaCache - { - private final AtomicReference> cachedReplicas = new AtomicReference<>(new ReplicaHolder<>(0, 4)); - - V get(long ringVersion, K keyToken) - { - ReplicaHolder replicaHolder = maybeClearAndGet(ringVersion); - if (replicaHolder == null) - return null; - - return replicaHolder.replicas.get(keyToken); - } - - void put(long ringVersion, K keyToken, V endpoints) - { - ReplicaHolder current = maybeClearAndGet(ringVersion); - if (current != null) - { - // if we have the same ringVersion, but already know about the keyToken the endpoints should be the same - current.replicas.putIfAbsent(keyToken, endpoints); - } - } - - ReplicaHolder maybeClearAndGet(long ringVersion) - { - ReplicaHolder current = cachedReplicas.get(); - if (ringVersion == current.ringVersion) - return current; - else if (ringVersion < current.ringVersion) // things have already moved on - return null; - - // If ring version has changed, create a fresh replica holder and try to replace the current one. - // This may race with other threads that have the same new ring version and one will win and the loosers - // will be garbage collected - ReplicaHolder cleaned = new ReplicaHolder<>(ringVersion, current.replicas.size()); - cachedReplicas.compareAndSet(current, cleaned); - - // A new ring version may have come along while making the new holder, so re-check the - // reference and return the ring version if the same, otherwise return null as there is no point - // in using it. - current = cachedReplicas.get(); - if (ringVersion == current.ringVersion) - return current; - else - return null; - } - } - - static class ReplicaHolder - { - private final long ringVersion; - private final NonBlockingHashMap replicas; - - ReplicaHolder(long ringVersion, int expectedEntries) - { - this.ringVersion = ringVersion; - this.replicas = new NonBlockingHashMap<>(expectedEntries); + throw new ConfigurationException(String.format("Unrecognized strategy option {%s} passed to %s for keyspace %s. Expected options: %s", key, getClass().getSimpleName(), keyspaceName, expectedOptions)); } } } diff --git a/src/java/org/apache/cassandra/locator/CMSPlacementStrategy.java b/src/java/org/apache/cassandra/locator/CMSPlacementStrategy.java new file mode 100644 index 0000000000..ea4f0cbb9a --- /dev/null +++ b/src/java/org/apache/cassandra/locator/CMSPlacementStrategy.java @@ -0,0 +1,155 @@ +/* + * 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.locator; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.ownership.TokenMap; + +import static org.apache.cassandra.locator.SimpleStrategy.REPLICATION_FACTOR; + +/** + * CMS Placement Strategy is how CMS keeps the number of its members at a configured level, given current + * cluster topolgy. It allows to add and remove CMS members when cluster topology changes. For example, during + * node replacement or decommission. + */ +public interface CMSPlacementStrategy +{ + Set reconfigure(Set currentCms, ClusterMetadata metadata); + + static CMSPlacementStrategy fromReplicationParams(ReplicationParams params, Predicate filter) + { + if (params.isMeta()) + { + assert !params.options.containsKey(REPLICATION_FACTOR); + Map dcRf = new HashMap<>(); + for (Map.Entry entry : params.options.entrySet()) + { + String dc = entry.getKey(); + ReplicationFactor rf = ReplicationFactor.fromString(entry.getValue()); + dcRf.put(dc, rf.fullReplicas); + } + return new DatacenterAware(dcRf, filter); + } + else + { + throw new IllegalStateException("Can't parse the params: " + params); + } + } + + /** + * Default reconfiguration strategy: attempts to achieve rack diversity, while keeping CMS placements + * close to how "regular" data would get replicated to keep the bounces safe, as long as the user + * bounces at most `f` members of the replica group, where `f = RF/2`. + */ + class DatacenterAware implements CMSPlacementStrategy + { + public final Map rf; + public final BiFunction filter; + + public DatacenterAware(Map rf, Predicate filter) + { + this(rf, new DefaultNodeFilter(filter)); + } + + @VisibleForTesting + public DatacenterAware(Map rf, BiFunction filter) + { + this.rf = rf; + this.filter = filter; + } + + public Set reconfigure(Set currentCms, ClusterMetadata metadata) + { + Map rf = new HashMap<>(this.rf.size()); + for (Map.Entry e : this.rf.entrySet()) + { + Collection nodesInDc = metadata.directory.allDatacenterEndpoints().get(e.getKey()); + if (nodesInDc == null) + throw new IllegalStateException(String.format("There are no nodes in %s datacenter", e.getKey())); + if (nodesInDc.size() < e.getValue()) + throw new Transformation.RejectedTransformationException(String.format("There are not enough nodes in %s datacenter to satisfy replication factor", e.getKey())); + + rf.put(e.getKey(), ReplicationFactor.fullOnly(e.getValue())); + } + + Directory directory = metadata.directory; + TokenMap tokenMap = metadata.tokenMap; + for (NodeId peerId : metadata.directory.peerIds()) + { + if (!filter.apply(metadata, peerId)) + { + directory = directory.without(peerId); + tokenMap = tokenMap.unassignTokens(peerId); + } + } + + EndpointsForRange endpoints = NetworkTopologyStrategy.calculateNaturalReplicas(EntireRange.entireRange.left, + EntireRange.entireRange, + directory, + tokenMap, + rf); + + return endpoints.endpoints().stream().map(metadata.directory::peerId).collect(Collectors.toSet()); + } + } + + class DefaultNodeFilter implements BiFunction + { + private final Predicate filter; + + public DefaultNodeFilter(Predicate filter) + { + this.filter = filter; + } + + public Boolean apply(ClusterMetadata metadata, NodeId nodeId) + { + if (!FailureDetector.instance.isAlive(metadata.directory.endpoint(nodeId))) + return false; + + if (metadata.directory.peerState(nodeId) != NodeState.JOINED) + return false; + + if (metadata.inProgressSequences.contains(nodeId)) + return false; + + if (!filter.test(nodeId)) + return false; + + return true; + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/locator/Endpoints.java b/src/java/org/apache/cassandra/locator/Endpoints.java index 1561db2bce..3dd418dbb6 100644 --- a/src/java/org/apache/cassandra/locator/Endpoints.java +++ b/src/java/org/apache/cassandra/locator/Endpoints.java @@ -97,6 +97,11 @@ public abstract class Endpoints> extends AbstractReplicaC return byEndpoint().get(self); } + public boolean containsSelf() + { + return selfIfPresent() != null; + } + /** * @return a collection without the provided endpoints, otherwise in the same order as this collection */ diff --git a/src/java/org/apache/cassandra/locator/EndpointsByReplica.java b/src/java/org/apache/cassandra/locator/EndpointsByReplica.java index 9590842c58..9c4636082f 100644 --- a/src/java/org/apache/cassandra/locator/EndpointsByReplica.java +++ b/src/java/org/apache/cassandra/locator/EndpointsByReplica.java @@ -21,12 +21,24 @@ package org.apache.cassandra.locator; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; +import java.io.IOException; import java.util.Map; +import static org.apache.cassandra.dht.AbstractBounds.tokenSerializer; + public class EndpointsByReplica extends ReplicaMultimap { + public static final Serializer serializer = new Serializer(); public EndpointsByReplica(Map map) { super(map); @@ -60,4 +72,56 @@ public class EndpointsByReplica extends ReplicaMultimap + { + @Override + public void serialize(EndpointsByReplica t, DataOutputPlus out, int version) throws IOException + { + out.writeUnsignedVInt32(t.map.size()); + for (Map.Entry entry : t.map.entrySet()) + { + Replica.serializer.serialize(entry.getKey(), out, version); + EndpointsForRange efr = entry.getValue(); + tokenSerializer.serialize(efr.range(), out, version); + out.writeUnsignedVInt32(efr.size()); + for (Replica replica : efr) + Replica.serializer.serialize(replica, out, version); + } + } + + @Override + public EndpointsByReplica deserialize(DataInputPlus in, int version) throws IOException + { + int size = in.readUnsignedVInt32(); + EndpointsByReplica.Builder builder = new EndpointsByReplica.Builder(); + for (int i = 0; i < size; i++) + { + Replica replica = Replica.serializer.deserialize(in, version); + Range range = (Range) tokenSerializer.deserialize(in, IPartitioner.global(), version); + int efrSize = in.readUnsignedVInt32(); + EndpointsForRange.Builder efrBuilder = new EndpointsForRange.Builder(range, efrSize); + for (int j = 0; j < efrSize; j++) + efrBuilder.add(Replica.serializer.deserialize(in, version), Conflict.NONE); + builder.putAll(replica, efrBuilder.build(), Conflict.NONE); + + } + return builder.build(); + } + + @Override + public long serializedSize(EndpointsByReplica t, int version) + { + long size = TypeSizes.sizeofVInt(t.map.size()); + for (Map.Entry entry : t.map.entrySet()) + { + size += Replica.serializer.serializedSize(entry.getKey(), version); + EndpointsForRange efr = entry.getValue(); + size += tokenSerializer.serializedSize(efr.range(), version); + size += TypeSizes.sizeofVInt(efr.size()); + for (Replica replica : efr) + size += Replica.serializer.serializedSize(replica, version); + } + return size; + } + } } diff --git a/src/java/org/apache/cassandra/locator/EndpointsForRange.java b/src/java/org/apache/cassandra/locator/EndpointsForRange.java index 7039df055f..f7aefb8dfd 100644 --- a/src/java/org/apache/cassandra/locator/EndpointsForRange.java +++ b/src/java/org/apache/cassandra/locator/EndpointsForRange.java @@ -71,8 +71,11 @@ public class EndpointsForRange extends Endpoints { if (newList.isEmpty()) return empty(range); ReplicaMap byEndpoint = null; - if (this.byEndpoint != null && list.isSubList(newList)) - byEndpoint = this.byEndpoint.forSubList(newList); + if (this.byEndpoint != null) + if (list.isSubList(newList)) + byEndpoint = this.byEndpoint.forSubList(newList); + else + byEndpoint = endpointMap(newList); return new EndpointsForRange(range, newList, byEndpoint); } diff --git a/src/java/org/apache/cassandra/locator/EndpointsForToken.java b/src/java/org/apache/cassandra/locator/EndpointsForToken.java index 70cd76325c..9b616ecd07 100644 --- a/src/java/org/apache/cassandra/locator/EndpointsForToken.java +++ b/src/java/org/apache/cassandra/locator/EndpointsForToken.java @@ -22,8 +22,8 @@ import com.google.common.base.Preconditions; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; import java.util.Arrays; import java.util.Collection; @@ -156,33 +156,15 @@ public class EndpointsForToken extends Endpoints return builder(token, replicas.size()).addAll(replicas).build(); } - public static EndpointsForToken natural(Keyspace keyspace, Token token) + public static EndpointsForToken copyOf(Token token, Iterable replicas) { - return keyspace.getReplicationStrategy().getNaturalReplicasForToken(token); + if (!replicas.iterator().hasNext()) return empty(token); + return builder(token).addAll(replicas).build(); } - public static EndpointsForToken natural(AbstractReplicationStrategy replicationStrategy, Token token) + public static VersionedEndpoints.ForToken natural(Keyspace keyspace, Token token) { - return replicationStrategy.getNaturalReplicasForToken(token); + return ClusterMetadata.current().placements.get(keyspace.getMetadata().params.replication).reads.forToken(token); } - public static EndpointsForToken natural(TableMetadata table, Token token) - { - return natural(Keyspace.open(table.keyspace), token); - } - - public static EndpointsForToken pending(TableMetadata table, Token token) - { - return pending(table.keyspace, token); - } - - public static EndpointsForToken pending(Keyspace keyspace, Token token) - { - return pending(keyspace.getName(), token); - } - - public static EndpointsForToken pending(String keyspace, Token token) - { - return StorageService.instance.getTokenMetadata().pendingEndpointsForToken(token, keyspace); - } } diff --git a/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java b/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java index 5aa7791e63..bbd110588b 100644 --- a/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java +++ b/src/java/org/apache/cassandra/locator/GossipingPropertyFileSnitch.java @@ -19,17 +19,16 @@ package org.apache.cassandra.locator; import java.util.concurrent.atomic.AtomicReference; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.utils.FBUtilities; @@ -37,14 +36,10 @@ public class GossipingPropertyFileSnitch extends AbstractNetworkTopologySnitch// { private static final Logger logger = LoggerFactory.getLogger(GossipingPropertyFileSnitch.class); - private PropertyFileSnitch psnitch; - private final String myDC; private final String myRack; private final boolean preferLocal; private final AtomicReference snitchHelperReference; - - private Map> savedEndpoints; private static final String DEFAULT_DC = "UNKNOWN_DC"; private static final String DEFAULT_RACK = "UNKNOWN_RACK"; @@ -56,16 +51,6 @@ public class GossipingPropertyFileSnitch extends AbstractNetworkTopologySnitch// myRack = properties.get("rack", DEFAULT_RACK).trim(); preferLocal = Boolean.parseBoolean(properties.get("prefer_local", "false")); snitchHelperReference = new AtomicReference<>(); - - try - { - psnitch = new PropertyFileSnitch(); - logger.info("Loaded {} for compatibility", PropertyFileSnitch.SNITCH_PROPERTIES_FILENAME); - } - catch (ConfigurationException e) - { - logger.info("Unable to load {}; compatibility mode disabled", PropertyFileSnitch.SNITCH_PROPERTIES_FILENAME); - } } private static SnitchProperties loadConfiguration() throws ConfigurationException @@ -88,21 +73,11 @@ public class GossipingPropertyFileSnitch extends AbstractNetworkTopologySnitch// if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return myDC; - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState == null || epState.getApplicationState(ApplicationState.DC) == null) - { - if (psnitch == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("data_center"); - return DEFAULT_DC; - } - else - return psnitch.getDatacenter(endpoint); - } - return epState.getApplicationState(ApplicationState.DC).value; + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return DEFAULT_DC; + return metadata.directory.location(nodeId).datacenter; } /** @@ -116,21 +91,11 @@ public class GossipingPropertyFileSnitch extends AbstractNetworkTopologySnitch// if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) return myRack; - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState == null || epState.getApplicationState(ApplicationState.RACK) == null) - { - if (psnitch == null) - { - if (savedEndpoints == null) - savedEndpoints = SystemKeyspace.loadDcRackInfo(); - if (savedEndpoints.containsKey(endpoint)) - return savedEndpoints.get(endpoint).get("rack"); - return DEFAULT_RACK; - } - else - return psnitch.getRack(endpoint); - } - return epState.getApplicationState(ApplicationState.RACK).value; + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return DEFAULT_RACK; + return metadata.directory.location(nodeId).rack; } public void gossiperStarting() diff --git a/src/java/org/apache/cassandra/locator/InetAddressAndPort.java b/src/java/org/apache/cassandra/locator/InetAddressAndPort.java index c954a05b1c..50f3368b20 100644 --- a/src/java/org/apache/cassandra/locator/InetAddressAndPort.java +++ b/src/java/org/apache/cassandra/locator/InetAddressAndPort.java @@ -37,6 +37,7 @@ import com.google.common.net.HostAndPort; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; @@ -219,6 +220,17 @@ public final class InetAddressAndPort extends InetSocketAddress implements Compa return getByNameOverrideDefaults(name, null); } + public static InetAddressAndPort getByNameUnchecked(String name) + { + try + { + return getByNameOverrideDefaults(name, null); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } public static List getAllByName(String name) throws UnknownHostException { @@ -316,6 +328,26 @@ public final class InetAddressAndPort extends InetSocketAddress implements Compa return defaultPort; } + public static final class MetadataSerializer implements org.apache.cassandra.tcm.serialization.MetadataSerializer + { + public static final MetadataSerializer serializer = new MetadataSerializer(); + private static final int SERDE_VERSION = MessagingService.Version.VERSION_40.value; + + public void serialize(InetAddressAndPort t, DataOutputPlus out, Version version) throws IOException + { + Serializer.inetAddressAndPortSerializer.serialize(t, out, SERDE_VERSION); + } + + public InetAddressAndPort deserialize(DataInputPlus in, Version version) throws IOException + { + return Serializer.inetAddressAndPortSerializer.deserialize(in, SERDE_VERSION); + } + + public long serializedSize(InetAddressAndPort t, Version version) + { + return Serializer.inetAddressAndPortSerializer.serializedSize(t, SERDE_VERSION); + } + } /** * As of version 4.0 the endpoint description includes a port number as an unsigned short */ diff --git a/src/java/org/apache/cassandra/locator/LocalStrategy.java b/src/java/org/apache/cassandra/locator/LocalStrategy.java index 0e3a9185fe..c98cbc2b94 100644 --- a/src/java/org/apache/cassandra/locator/LocalStrategy.java +++ b/src/java/org/apache/cassandra/locator/LocalStrategy.java @@ -17,67 +17,40 @@ */ package org.apache.cassandra.locator; -import java.util.Collections; -import java.util.Collection; +import java.util.List; import java.util.Map; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.dht.RingPosition; +import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; -public class LocalStrategy extends AbstractReplicationStrategy +public class LocalStrategy extends SystemStrategy { private static final ReplicationFactor RF = ReplicationFactor.fullOnly(1); - private final EndpointsForRange replicas; - public LocalStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map configOptions) + public LocalStrategy(String keyspaceName, Map configOptions) { - super(keyspaceName, tokenMetadata, snitch, configOptions); - replicas = EndpointsForRange.of( - new Replica(FBUtilities.getBroadcastAddressAndPort(), - DatabaseDescriptor.getPartitioner().getMinimumToken(), - DatabaseDescriptor.getPartitioner().getMinimumToken(), - true - ) - ); + super(keyspaceName, configOptions); } - /** - * We need to override this even if we override calculateNaturalReplicas, - * because the default implementation depends on token calculations but - * LocalStrategy may be used before tokens are set up. - */ @Override - public EndpointsForRange getNaturalReplicas(RingPosition searchPosition) + public EndpointsForRange calculateNaturalReplicas(Token token, ClusterMetadata metadata) { - return replicas; + return EntireRange.localReplicas; } - public EndpointsForRange calculateNaturalReplicas(Token token, TokenMetadata metadata) + @Override + public DataPlacement calculateDataPlacement(Epoch epoch, List> ranges, ClusterMetadata metadata) { - return replicas; + return EntireRange.placement; } + @Override public ReplicationFactor getReplicationFactor() { return RF; } - - public void validateOptions() throws ConfigurationException - { - } - - @Override - public void maybeWarnOnOptions() - { - } - - @Override - public Collection recognizedOptions() - { - // LocalStrategy doesn't expect any options. - return Collections.emptySet(); - } } diff --git a/src/java/org/apache/cassandra/locator/MetaStrategy.java b/src/java/org/apache/cassandra/locator/MetaStrategy.java new file mode 100644 index 0000000000..ac9685227e --- /dev/null +++ b/src/java/org/apache/cassandra/locator/MetaStrategy.java @@ -0,0 +1,87 @@ +/* + * 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.locator; + +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ownership.DataPlacement; + +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +/** + * MetaStrategy is designed for distributed cluster metadata keyspace, and should not be used by + * the users directly. This strategy allows a configurable number of nodes to own an entire range and + * be responsible for cluster metadata queries. + * + * Nodes are added to and removed from placements using ReconfigureCMS sequence, and PrepareCMSReconfiguration/ + * AdvanceCMSReconfiguration, according to CMSPlacementStrategy derived from params specified in + * options of MetaStrategy. + */ +public class MetaStrategy extends SystemStrategy +{ + public MetaStrategy(String keyspaceName, Map configOptions) + { + super(keyspaceName, configOptions); + } + + @Override + public EndpointsForRange calculateNaturalReplicas(Token token, ClusterMetadata metadata) + { + return metadata.placements.get(ReplicationParams.meta(metadata)).reads.forRange(entireRange).get(); + } + + @Override + public DataPlacement calculateDataPlacement(Epoch epoch, List> ranges, ClusterMetadata metadata) + { + return metadata.placements.get(ReplicationParams.meta(metadata)); + } + + @Override + public ReplicationFactor getReplicationFactor() + { + ClusterMetadata metadata = ClusterMetadata.current(); + int rf = metadata.placements.get(ReplicationParams.meta(metadata)).writes.forRange(entireRange).get().byEndpoint.size(); + return ReplicationFactor.fullOnly(rf); + } + + @Override + public boolean hasSameSettings(AbstractReplicationStrategy other) + { + return getClass().equals(other.getClass()); + } + + @Override + public boolean hasTransientReplicas() + { + return false; + } + + public String toString() + { + return "MetaStrategy{" + + "configOptions=" + configOptions + + ", keyspaceName='" + keyspaceName + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java index 1615d8e5f5..d48ee31610 100644 --- a/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java +++ b/src/java/org/apache/cassandra/locator/NetworkTopologyStrategy.java @@ -17,31 +17,42 @@ */ package org.apache.cassandra.locator; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.dht.Datacenters; import org.apache.cassandra.dht.Range; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.TokenMetadata.Topology; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.ownership.TokenMap; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; - -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import com.google.common.collect.Sets; /** *

    @@ -66,9 +77,9 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy private final ReplicationFactor aggregateRf; private static final Logger logger = LoggerFactory.getLogger(NetworkTopologyStrategy.class); - public NetworkTopologyStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map configOptions) throws ConfigurationException + public NetworkTopologyStrategy(String keyspaceName, Map configOptions) throws ConfigurationException { - super(keyspaceName, tokenMetadata, snitch, configOptions); + super(keyspaceName, configOptions); int replicas = 0; int trans = 0; @@ -105,7 +116,7 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy * For efficiency the set is shared between the instances, using the location pair (dc, rack) to make sure * clashing names aren't a problem. */ - Set> racks; + Set racks; /** Number of replicas left to fill from this DC. */ int rfLeft; @@ -116,7 +127,7 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy int rackCount, int nodeCount, EndpointsForRange.Builder replicas, - Set> racks) + Set racks) { this.replicas = replicas; this.racks = racks; @@ -136,7 +147,7 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy * Attempts to add an endpoint to the replicas for this datacenter, adding to the replicas set if successful. * Returns true if the endpoint was added, and this datacenter does not require further replicas. */ - boolean addEndpointAndCheckIfDone(InetAddressAndPort ep, Pair location, Range replicatedRange) + boolean addEndpointAndCheckIfDone(InetAddressAndPort ep, Location location, Range replicatedRange) { if (done()) return false; @@ -172,27 +183,54 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy } } + @Override + public DataPlacement calculateDataPlacement(Epoch epoch, List> ranges, ClusterMetadata metadata) + { + return calculateDataPlacement(epoch, ranges, metadata.directory, metadata.tokenMap); + } + + private DataPlacement calculateDataPlacement(Epoch epoch, + List> ranges, + Directory directory, + TokenMap tokenMap) + { + PlacementForRange.Builder builder = PlacementForRange.builder(); + for (Range range : ranges) + { + EndpointsForRange endpointsForRange = calculateNaturalReplicas(range.right, + range, + directory, + tokenMap, + datacenters); + builder.withReplicaGroup(VersionedEndpoints.forRange(epoch, endpointsForRange)); + } + + PlacementForRange built = builder.build(); + return new DataPlacement(built, built); + } + /** * calculate endpoints in one pass through the tokens by tracking our progress in each DC. */ @Override - public EndpointsForRange calculateNaturalReplicas(Token searchToken, TokenMetadata tokenMetadata) + public EndpointsForRange calculateNaturalReplicas(Token searchToken, ClusterMetadata metadata) { // we want to preserve insertion order so that the first added endpoint becomes primary - ArrayList sortedTokens = tokenMetadata.sortedTokens(); - Token replicaEnd = TokenMetadata.firstToken(sortedTokens, searchToken); - Token replicaStart = tokenMetadata.getPredecessor(replicaEnd); - Range replicatedRange = new Range<>(replicaStart, replicaEnd); + Range replicatedRange = TokenRingUtils.getRange(metadata.tokenMap.tokens(), searchToken); + return calculateNaturalReplicas(searchToken, replicatedRange, metadata.directory, metadata.tokenMap, datacenters); + } + public static EndpointsForRange calculateNaturalReplicas(Token searchToken, + Range replicatedRange, + Directory directory, + TokenMap tokens, + Map datacenters) + { EndpointsForRange.Builder builder = new EndpointsForRange.Builder(replicatedRange); - Set> seenRacks = new HashSet<>(); + Set seenRacks = new HashSet<>(); - Topology topology = tokenMetadata.getTopology(); - // all endpoints in each DC, so we can check when we have exhausted all the members of a DC - Multimap allEndpoints = topology.getDatacenterEndpoints(); - // all racks in a DC so we can check when we have exhausted all racks in a DC - Map> racks = topology.getDatacenterRacks(); - assert !allEndpoints.isEmpty() && !racks.isEmpty() : "not aware of any cluster members"; + // Check if we have exhausted all the members/racks of a DC + assert !directory.allDatacenterEndpoints().isEmpty() && !directory.allDatacenterRacks().isEmpty() : "not aware of any cluster members"; int dcsToFill = 0; Map dcs = new HashMap<>(datacenters.size() * 2); @@ -202,35 +240,40 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy { String dc = en.getKey(); ReplicationFactor rf = en.getValue(); - int nodeCount = sizeOrZero(allEndpoints.get(dc)); + int nodeCount = sizeOrZero(directory.datacenterEndpoints(dc)); if (rf.allReplicas <= 0 || nodeCount <= 0) continue; - DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, sizeOrZero(racks.get(dc)), nodeCount, builder, seenRacks); + DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, + sizeOrZero(directory.datacenterRacks(dc)), + nodeCount, + builder, + seenRacks); dcs.put(dc, dcEndpoints); ++dcsToFill; } - Iterator tokenIter = TokenMetadata.ringIterator(sortedTokens, searchToken, false); + Iterator tokenIter = TokenRingUtils.ringIterator(tokens.tokens(), searchToken, false); while (dcsToFill > 0 && tokenIter.hasNext()) { Token next = tokenIter.next(); - InetAddressAndPort ep = tokenMetadata.getEndpoint(next); - Pair location = topology.getLocation(ep); - DatacenterEndpoints dcEndpoints = dcs.get(location.left); + NodeId owner = tokens.owner(next); + InetAddressAndPort ep = directory.endpoint(owner); + Location location = directory.location(owner); + DatacenterEndpoints dcEndpoints = dcs.get(location.datacenter); if (dcEndpoints != null && dcEndpoints.addEndpointAndCheckIfDone(ep, location, replicatedRange)) --dcsToFill; } return builder.build(); } - private int sizeOrZero(Multimap collection) + private static int sizeOrZero(Multimap collection) { return collection != null ? collection.asMap().size() : 0; } - private int sizeOrZero(Collection collection) + private static int sizeOrZero(Collection collection) { return collection != null ? collection.size() : 0; } @@ -253,10 +296,10 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy } @Override - public Collection recognizedOptions() + public Collection recognizedOptions(ClusterMetadata metadata) { // only valid options are valid DC names. - return Datacenters.getValidDatacenters(); + return Datacenters.getValidDatacenters(metadata); } /** @@ -296,7 +339,7 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy if (replication != null) { ReplicationFactor defaultReplicas = ReplicationFactor.fromString(replication); - Datacenters.getValidDatacenters() + Datacenters.getValidDatacenters(ClusterMetadata.current()) .forEach(dc -> options.putIfAbsent(dc, defaultReplicas.toParseableString())); } @@ -304,7 +347,7 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy } @Override - protected void validateExpectedOptions() throws ConfigurationException + public void validateExpectedOptions(ClusterMetadata metadata) throws ConfigurationException { // Do not accept query with no data centers specified. if (this.configOptions.isEmpty()) @@ -313,11 +356,11 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy } // Validate the data center names - super.validateExpectedOptions(); + super.validateExpectedOptions(metadata); if (keyspaceName.equalsIgnoreCase(SchemaConstants.AUTH_KEYSPACE_NAME)) { - Set differenceSet = Sets.difference((Set) recognizedOptions(), configOptions.keySet()); + Set differenceSet = Sets.difference(metadata.directory.knownDatacenters(), configOptions.keySet()); if (!differenceSet.isEmpty()) { throw new ConfigurationException("Following datacenters have active nodes and must be present in replication options for keyspace " + SchemaConstants.AUTH_KEYSPACE_NAME + ": " + differenceSet.toString()); @@ -343,15 +386,15 @@ public class NetworkTopologyStrategy extends AbstractReplicationStrategy { if (!SchemaConstants.isSystemKeyspace(keyspaceName)) { - ImmutableMultimap dcsNodes = Multimaps.index(StorageService.instance.getTokenMetadata().getAllMembers(), snitch::getDatacenter); + Directory directory = ClusterMetadata.current().directory; + Multimap dcsNodes = directory.allDatacenterEndpoints(); for (Entry e : this.configOptions.entrySet()) { - String dc = e.getKey(); ReplicationFactor rf = getReplicationFactor(dc); Guardrails.minimumReplicationFactor.guard(rf.fullReplicas, keyspaceName, false, state); Guardrails.maximumReplicationFactor.guard(rf.fullReplicas, keyspaceName, false, state); - int nodeCount = dcsNodes.get(dc).size(); + int nodeCount = dcsNodes.containsKey(dc) ? dcsNodes.get(dc).size() : 0; // nodeCount==0 on many tests if (rf.fullReplicas > nodeCount && nodeCount != 0) { diff --git a/src/java/org/apache/cassandra/locator/PendingRangeMaps.java b/src/java/org/apache/cassandra/locator/PendingRangeMaps.java deleted file mode 100644 index 14870c3b10..0000000000 --- a/src/java/org/apache/cassandra/locator/PendingRangeMaps.java +++ /dev/null @@ -1,212 +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.locator; - -import com.google.common.collect.Iterators; - -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; - -import java.util.*; - -import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR; - -public class PendingRangeMaps implements Iterable, EndpointsForRange.Builder>> -{ - /** - * We have for NavigableMap to be able to search for ranges containing a token efficiently. - * - * First two are for non-wrap-around ranges, and the last two are for wrap-around ranges. - */ - // ascendingMap will sort the ranges by the ascending order of right token - private final NavigableMap, EndpointsForRange.Builder> ascendingMap; - - /** - * sorting end ascending, if ends are same, sorting begin descending, so that token (end, end) will - * come before (begin, end] with the same end, and (begin, end) will be selected in the tailMap. - */ - private static final Comparator> ascendingComparator = (o1, o2) -> { - int res = o1.right.compareTo(o2.right); - if (res != 0) - return res; - - return o2.left.compareTo(o1.left); - }; - - // ascendingMap will sort the ranges by the descending order of left token - private final NavigableMap, EndpointsForRange.Builder> descendingMap; - - /** - * sorting begin descending, if begins are same, sorting end descending, so that token (begin, begin) will - * come after (begin, end] with the same begin, and (begin, end) won't be selected in the tailMap. - */ - private static final Comparator> descendingComparator = (o1, o2) -> { - int res = o2.left.compareTo(o1.left); - if (res != 0) - return res; - - // if left tokens are same, sort by the descending of the right tokens. - return o2.right.compareTo(o1.right); - }; - - // these two maps are for warp around ranges. - private final NavigableMap, EndpointsForRange.Builder> ascendingMapForWrapAround; - - /** - * for wrap around range (begin, end], which begin > end. - * Sorting end ascending, if ends are same, sorting begin ascending, - * so that token (end, end) will come before (begin, end] with the same end, and (begin, end] will be selected in - * the tailMap. - */ - private static final Comparator> ascendingComparatorForWrapAround = (o1, o2) -> { - int res = o1.right.compareTo(o2.right); - if (res != 0) - return res; - - return o1.left.compareTo(o2.left); - }; - - private final NavigableMap, EndpointsForRange.Builder> descendingMapForWrapAround; - - /** - * for wrap around ranges, which begin > end. - * Sorting end ascending, so that token (begin, begin) will come after (begin, end] with the same begin, - * and (begin, end) won't be selected in the tailMap. - */ - private static final Comparator> descendingComparatorForWrapAround = (o1, o2) -> { - int res = o2.left.compareTo(o1.left); - if (res != 0) - return res; - return o1.right.compareTo(o2.right); - }; - - public PendingRangeMaps() - { - this.ascendingMap = new TreeMap<>(ascendingComparator); - this.descendingMap = new TreeMap<>(descendingComparator); - this.ascendingMapForWrapAround = new TreeMap<>(ascendingComparatorForWrapAround); - this.descendingMapForWrapAround = new TreeMap<>(descendingComparatorForWrapAround); - } - - static final void addToMap(Range range, - Replica replica, - NavigableMap, EndpointsForRange.Builder> ascendingMap, - NavigableMap, EndpointsForRange.Builder> descendingMap) - { - EndpointsForRange.Builder replicas = ascendingMap.get(range); - if (replicas == null) - { - replicas = new EndpointsForRange.Builder(range,1); - ascendingMap.put(range, replicas); - descendingMap.put(range, replicas); - } - replicas.add(replica, Conflict.DUPLICATE); - } - - public void addPendingRange(Range range, Replica replica) - { - if (Range.isWrapAround(range.left, range.right)) - { - addToMap(range, replica, ascendingMapForWrapAround, descendingMapForWrapAround); - } - else - { - addToMap(range, replica, ascendingMap, descendingMap); - } - } - - static final void addIntersections(EndpointsForToken.Builder replicasToAdd, - NavigableMap, EndpointsForRange.Builder> smallerMap, - NavigableMap, EndpointsForRange.Builder> biggerMap) - { - // find the intersection of two sets - for (Range range : smallerMap.keySet()) - { - EndpointsForRange.Builder replicas = biggerMap.get(range); - if (replicas != null) - { - replicasToAdd.addAll(replicas); - } - } - } - - public EndpointsForToken pendingEndpointsFor(Token token) - { - EndpointsForToken.Builder replicas = EndpointsForToken.builder(token); - - Range searchRange = new Range<>(token, token); - - // search for non-wrap-around maps - NavigableMap, EndpointsForRange.Builder> ascendingTailMap = ascendingMap.tailMap(searchRange, true); - NavigableMap, EndpointsForRange.Builder> descendingTailMap = descendingMap.tailMap(searchRange, false); - - // add intersections of two maps - if (ascendingTailMap.size() < descendingTailMap.size()) - { - addIntersections(replicas, ascendingTailMap, descendingTailMap); - } - else - { - addIntersections(replicas, descendingTailMap, ascendingTailMap); - } - - // search for wrap-around sets - ascendingTailMap = ascendingMapForWrapAround.tailMap(searchRange, true); - descendingTailMap = descendingMapForWrapAround.tailMap(searchRange, false); - - // add them since they are all necessary. - for (Map.Entry, EndpointsForRange.Builder> entry : ascendingTailMap.entrySet()) - { - replicas.addAll(entry.getValue()); - } - for (Map.Entry, EndpointsForRange.Builder> entry : descendingTailMap.entrySet()) - { - replicas.addAll(entry.getValue()); - } - - return replicas.build(); - } - - public String printPendingRanges() - { - StringBuilder sb = new StringBuilder(); - - for (Map.Entry, EndpointsForRange.Builder> entry : this) - { - Range range = entry.getKey(); - - for (Replica replica : entry.getValue()) - { - sb.append(replica).append(':').append(range); - sb.append(LINE_SEPARATOR.getString()); - } - } - - return sb.toString(); - } - - @Override - public Iterator, EndpointsForRange.Builder>> iterator() - { - return Iterators.concat(ascendingMap.entrySet().iterator(), ascendingMapForWrapAround.entrySet().iterator()); - } -} diff --git a/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java b/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java index 3a9b161356..430779f468 100644 --- a/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java +++ b/src/java/org/apache/cassandra/locator/PropertyFileSnitch.java @@ -19,21 +19,19 @@ package org.apache.cassandra.locator; import java.io.InputStream; import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import java.util.Properties; -import java.util.Set; -import com.google.common.collect.Sets; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.ResourceWatcher; -import org.apache.cassandra.utils.WrappedRunnable; + import org.apache.commons.lang3.StringUtils; /** @@ -46,82 +44,43 @@ import org.apache.commons.lang3.StringUtils; * 10.21.119.14=DC3:RAC2 * 10.20.114.15=DC2:RAC2 * default=DC1:r1 + * + * Post CEP-21, only the local rack and DC are loaded from file. Each peer in the cluster is required to register + * itself with the Cluster Metadata Service and provide its Location (Rack + DC) before joining. During upgrades, + * this is done automatically with location derived from gossip state (ultimately from system.local). + * Once registered, the Rack & DC should not be changed but currently the only safeguards against this are the + * StartupChecks which validate the snitch against system.local. */ public class PropertyFileSnitch extends AbstractNetworkTopologySnitch { private static final Logger logger = LoggerFactory.getLogger(PropertyFileSnitch.class); public static final String SNITCH_PROPERTIES_FILENAME = "cassandra-topology.properties"; - private static final int DEFAULT_REFRESH_PERIOD_IN_SECONDS = 5; + // All the defaults + private static final String DEFAULT_PROPERTY = "default"; + @VisibleForTesting + public static final String DEFAULT_DC = "default"; + @VisibleForTesting + public static final String DEFAULT_RACK = "default"; - private static volatile Map endpointMap; - private static volatile String[] defaultDCRack; + private final Location local; - private volatile boolean gossipStarted; public PropertyFileSnitch() throws ConfigurationException { - this(DEFAULT_REFRESH_PERIOD_IN_SECONDS); + local = loadConfiguration(); } - public PropertyFileSnitch(int refreshPeriodInSeconds) throws ConfigurationException - { - reloadConfiguration(false); - - try - { - FBUtilities.resourceToFile(SNITCH_PROPERTIES_FILENAME); - Runnable runnable = new WrappedRunnable() - { - protected void runMayThrow() throws ConfigurationException - { - reloadConfiguration(true); - } - }; - ResourceWatcher.watch(SNITCH_PROPERTIES_FILENAME, runnable, refreshPeriodInSeconds * 1000); - } - catch (ConfigurationException ex) - { - logger.error("{} found, but does not look like a plain file. Will not watch it for changes", SNITCH_PROPERTIES_FILENAME); - } - } - - /** - * Get the raw information about an end point - * - * @param endpoint endpoint to process - * @return a array of string with the first index being the data center and the second being the rack - */ - public static String[] getEndpointInfo(InetAddressAndPort endpoint) - { - String[] rawEndpointInfo = getRawEndpointInfo(endpoint); - if (rawEndpointInfo == null) - throw new RuntimeException("Unknown host " + endpoint + " with no default configured"); - return rawEndpointInfo; - } - - private static String[] getRawEndpointInfo(InetAddressAndPort endpoint) - { - String[] value = endpointMap.get(endpoint); - if (value == null) - { - logger.trace("Could not find end point information for {}, will use default", endpoint); - return defaultDCRack; - } - return value; - } - - /** - * Return the data center for which an endpoint resides in - * - * @param endpoint the endpoint to process - * @return string of data center - */ public String getDatacenter(InetAddressAndPort endpoint) { - String[] info = getEndpointInfo(endpoint); - assert info != null : "No location defined for endpoint " + endpoint; - return info[0]; + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return local.datacenter; + + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return DEFAULT_DC; + return metadata.directory.location(nodeId).datacenter; } /** @@ -132,16 +91,34 @@ public class PropertyFileSnitch extends AbstractNetworkTopologySnitch */ public String getRack(InetAddressAndPort endpoint) { - String[] info = getEndpointInfo(endpoint); - assert info != null : "No location defined for endpoint " + endpoint; - return info[1]; + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return local.rack; + + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return DEFAULT_RACK; + return metadata.directory.location(nodeId).rack; } - public void reloadConfiguration(boolean isUpdate) throws ConfigurationException + private Location makeLocation(String value) { - HashMap reloadedMap = new HashMap<>(); - String[] reloadedDefaultDCRack = null; + if (value == null || value.isEmpty()) + return null; + String[] parts = value.split(":"); + if (parts.length < 2) + { + return new Location(DEFAULT_DC, DEFAULT_RACK); + } + else + { + return new Location(parts[0].trim(), parts[1].trim()); + } + } + + private Location loadConfiguration() throws ConfigurationException + { Properties properties = new Properties(); try (InputStream stream = getClass().getClassLoader().getResourceAsStream(SNITCH_PROPERTIES_FILENAME)) { @@ -152,115 +129,54 @@ public class PropertyFileSnitch extends AbstractNetworkTopologySnitch throw new ConfigurationException("Unable to read " + SNITCH_PROPERTIES_FILENAME, e); } + // may be null, which is ok unless config doesn't contain the location of the local node + Location defaultLocation = makeLocation(properties.getProperty(DEFAULT_PROPERTY)); + Location local = null; + InetAddressAndPort broadcastAddress = FBUtilities.getBroadcastAddressAndPort(); for (Map.Entry entry : properties.entrySet()) { String key = (String) entry.getKey(); String value = (String) entry.getValue(); + if (DEFAULT_PROPERTY.equals(key)) + continue; - if ("default".equals(key)) + String hostString = StringUtils.remove(key, '/'); + try { - String[] newDefault = value.split(":"); - if (newDefault.length < 2) - reloadedDefaultDCRack = new String[] { "default", "default" }; - else - reloadedDefaultDCRack = new String[] { newDefault[0].trim(), newDefault[1].trim() }; + InetAddressAndPort host = InetAddressAndPort.getByName(hostString); + if (host.equals(broadcastAddress)) + { + local = makeLocation(value); + break; + } + } + catch (UnknownHostException e) + { + throw new ConfigurationException("Unknown host " + hostString, e); + } + + } + + if (local == null) + { + if (defaultLocation == null) + { + throw new ConfigurationException(String.format("Snitch definitions at %s do not define a location for " + + "this node's broadcast address %s, nor does it provides a default", + SNITCH_PROPERTIES_FILENAME, broadcastAddress)); } else { - InetAddressAndPort host; - String hostString = StringUtils.remove(key, '/'); - try - { - host = InetAddressAndPort.getByName(hostString); - } - catch (UnknownHostException e) - { - throw new ConfigurationException("Unknown host " + hostString, e); - } - String[] token = value.split(":"); - if (token.length < 2) - token = new String[] { "default", "default" }; - else - token = new String[] { token[0].trim(), token[1].trim() }; - reloadedMap.put(host, token); - } - } - InetAddressAndPort broadcastAddress = FBUtilities.getBroadcastAddressAndPort(); - String[] localInfo = reloadedMap.get(broadcastAddress); - if (reloadedDefaultDCRack == null && localInfo == null) - throw new ConfigurationException(String.format("Snitch definitions at %s do not define a location for " + - "this node's broadcast address %s, nor does it provides a default", - SNITCH_PROPERTIES_FILENAME, broadcastAddress)); - // internode messaging code converts our broadcast address to local, - // make sure we can be found at that as well. - InetAddressAndPort localAddress = FBUtilities.getLocalAddressAndPort(); - if (!localAddress.equals(broadcastAddress) && !reloadedMap.containsKey(localAddress)) - reloadedMap.put(localAddress, localInfo); - - if (isUpdate && !livenessCheck(reloadedMap, reloadedDefaultDCRack)) - return; - - if (logger.isTraceEnabled()) - { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : reloadedMap.entrySet()) - sb.append(entry.getKey()).append(':').append(Arrays.toString(entry.getValue())).append(", "); - logger.trace("Loaded network topology from property file: {}", StringUtils.removeEnd(sb.toString(), ", ")); - } - - - defaultDCRack = reloadedDefaultDCRack; - endpointMap = reloadedMap; - if (StorageService.instance != null) // null check tolerates circular dependency; see CASSANDRA-4145 - { - if (isUpdate) - StorageService.instance.updateTopology(); - else - StorageService.instance.getTokenMetadata().invalidateCachedRings(); - } - - if (gossipStarted) - StorageService.instance.gossipSnitchInfo(); - } - - /** - * We cannot update rack or data-center for a live node, see CASSANDRA-10243. - * - * @param reloadedMap - the new map of hosts to dc:rack properties - * @param reloadedDefaultDCRack - the default dc:rack or null if no default - * @return true if we can continue updating (no live host had dc or rack updated) - */ - private static boolean livenessCheck(HashMap reloadedMap, String[] reloadedDefaultDCRack) - { - // If the default has changed we must check all live hosts but hopefully we will find a live - // host quickly and interrupt the loop. Otherwise we only check the live hosts that were either - // in the old set or in the new set - Set hosts = Arrays.equals(defaultDCRack, reloadedDefaultDCRack) - ? Sets.intersection(StorageService.instance.getLiveRingMembers(), // same default - Sets.union(endpointMap.keySet(), reloadedMap.keySet())) - : StorageService.instance.getLiveRingMembers(); // default updated - - for (InetAddressAndPort host : hosts) - { - String[] origValue = endpointMap.containsKey(host) ? endpointMap.get(host) : defaultDCRack; - String[] updateValue = reloadedMap.containsKey(host) ? reloadedMap.get(host) : reloadedDefaultDCRack; - - if (!Arrays.equals(origValue, updateValue)) - { - logger.error("Cannot update data center or rack from {} to {} for live host {}, property file NOT RELOADED", - origValue, - updateValue, - host); - return false; + logger.debug("Broadcast address {} was not present in snitch config, using default location {}. " + + "This only matters on first boot, before registering with the cluster metadata service", + broadcastAddress, defaultLocation); + return defaultLocation; } } - return true; - } - - @Override - public void gossiperStarting() - { - gossipStarted = true; + logger.debug("Loaded location {} for broadcast address {} from property file. " + + "This only matters on first boot, before registering with the cluster metadata service", + local, broadcastAddress); + return local; } } diff --git a/src/java/org/apache/cassandra/locator/RackInferringSnitch.java b/src/java/org/apache/cassandra/locator/RackInferringSnitch.java index 3429ad1aec..cc47e0e8fb 100644 --- a/src/java/org/apache/cassandra/locator/RackInferringSnitch.java +++ b/src/java/org/apache/cassandra/locator/RackInferringSnitch.java @@ -17,19 +17,51 @@ */ package org.apache.cassandra.locator; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.utils.FBUtilities; + /** * A simple endpoint snitch implementation that assumes datacenter and rack information is encoded * in the 2nd and 3rd octets of the ip address, respectively. + * As with all snitches post CEP-21, this retrieves Location for remote peers from ClusterMetadata. + * Local location is derived from (broadcast) ip address and added to ClusterMetadata during node + * registration. Every member of the cluster is required to do this, hence remote peers' Location + * can always be retrieved, consistently. */ public class RackInferringSnitch extends AbstractNetworkTopologySnitch { - public String getRack(InetAddressAndPort endpoint) + final Location local; + + public RackInferringSnitch() { - return Integer.toString(endpoint.getAddress().getAddress()[2] & 0xFF, 10); + InetAddressAndPort localAddress = FBUtilities.getBroadcastAddressAndPort(); + local = new Location(Integer.toString(localAddress.getAddress().getAddress()[1] & 0xFF, 10), + Integer.toString(localAddress.getAddress().getAddress()[2] & 0xFF, 10)); } public String getDatacenter(InetAddressAndPort endpoint) { - return Integer.toString(endpoint.getAddress().getAddress()[1] & 0xFF, 10); + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return local.datacenter; + + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return Integer.toString(endpoint.getAddress().getAddress()[1] & 0xFF, 10); + return metadata.directory.location(nodeId).datacenter; + } + + public String getRack(InetAddressAndPort endpoint) + { + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + return local.rack; + + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + return Integer.toString(endpoint.getAddress().getAddress()[2] & 0xFF, 10); + return metadata.directory.location(nodeId).rack; } } diff --git a/src/java/org/apache/cassandra/locator/RangesByEndpoint.java b/src/java/org/apache/cassandra/locator/RangesByEndpoint.java index 023d7ee2b4..ed3e2a7918 100644 --- a/src/java/org/apache/cassandra/locator/RangesByEndpoint.java +++ b/src/java/org/apache/cassandra/locator/RangesByEndpoint.java @@ -22,10 +22,26 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import java.io.IOException; import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; public class RangesByEndpoint extends ReplicaMultimap { + public static RangesByEndpoint EMPTY = new RangesByEndpoint.Builder().build(); + + public static final Serializer serializer = new Serializer(); + public RangesByEndpoint(Map map) { super(map); @@ -53,4 +69,63 @@ public class RangesByEndpoint extends ReplicaMultimap + { + public void serialize(RangesByEndpoint t, DataOutputPlus out, Version version) throws IOException + { + Set> entries = t.entrySet(); + out.writeInt(entries.size()); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (Map.Entry entry : entries) + { + InetAddressAndPort.MetadataSerializer.serializer.serialize(entry.getKey(), out, version); + AbstractReplicaCollection.ReplicaList replicas = entry.getValue().list; + out.writeInt(replicas.size()); + for (Replica r : replicas) + { + // TODO duplicated from IPartitioner.validate as we don't want to rely on DatabaseDescriptor + if (partitioner != r.range().left.getPartitioner()) + throw new AssertionError(String.format("Partitioner in range serialization. Expected %s, was %s.", + partitioner.getClass().getName(), + r.range().left.getPartitioner().getClass().getName())); + Range.serializer.serialize(r.range(), out, version); + out.writeBoolean(r.isFull()); + } + } + } + + public RangesByEndpoint deserialize(DataInputPlus in, Version version) throws IOException + { + RangesByEndpoint.Builder builder = new Builder(); + int size = in.readInt(); + for (int i=0; i range = Range.serializer.deserialize(in, version); + boolean full = in.readBoolean(); + builder.put(endpoint, new Replica(endpoint, range, full)); + } + } + return builder.build(); + } + + public long serializedSize(RangesByEndpoint t, Version version) + { + long size = TypeSizes.INT_SIZE; + for (Map.Entry entry : t.entrySet()) + { + size += InetAddressAndPort.MetadataSerializer.serializer.serializedSize(entry.getKey(), version); + size += TypeSizes.INT_SIZE; + for (Replica r : entry.getValue().list) + { + size += Range.serializer.serializedSize(r.range(), version); + size += TypeSizes.BOOL_SIZE; + } + } + return size; + } + } } diff --git a/src/java/org/apache/cassandra/locator/Replica.java b/src/java/org/apache/cassandra/locator/Replica.java index 4c58e64350..86f190e42b 100644 --- a/src/java/org/apache/cassandra/locator/Replica.java +++ b/src/java/org/apache/cassandra/locator/Replica.java @@ -18,15 +18,23 @@ package org.apache.cassandra.locator; +import java.io.IOException; import java.util.Objects; import java.util.Set; import com.google.common.base.Preconditions; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.dht.AbstractBounds.tokenSerializer; + /** * A Replica represents an owning node for a copy of a portion of the token ring. * @@ -45,6 +53,8 @@ import org.apache.cassandra.utils.FBUtilities; */ public final class Replica implements Comparable { + public static final IVersionedSerializer serializer = new Serializer(); + private final Range range; private final InetAddressAndPort endpoint; private final boolean full; @@ -168,7 +178,7 @@ public final class Replica implements Comparable public Replica decorateSubrange(Range subrange) { - Preconditions.checkArgument(range.contains(subrange)); + Preconditions.checkArgument(range.contains(subrange), range + " " + subrange); return new Replica(endpoint(), subrange, isFull()); } @@ -191,5 +201,33 @@ public final class Replica implements Comparable { return transientReplica(endpoint, new Range<>(start, end)); } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(Replica t, DataOutputPlus out, int version) throws IOException + { + tokenSerializer.serialize(t.range, out, version); + InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(t.endpoint, out, version); + out.writeBoolean(t.isFull()); + } + + @Override + public Replica deserialize(DataInputPlus in, int version) throws IOException + { + Range range = (Range) tokenSerializer.deserialize(in, IPartitioner.global(), version); + InetAddressAndPort endpoint = InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version); + boolean isFull = in.readBoolean(); + return new Replica(endpoint, range, isFull); + } + + @Override + public long serializedSize(Replica t, int version) + { + return tokenSerializer.serializedSize(t.range, version) + + InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(t.endpoint, version) + + TypeSizes.sizeof(t.isFull()); + } + } } diff --git a/src/java/org/apache/cassandra/locator/ReplicaLayout.java b/src/java/org/apache/cassandra/locator/ReplicaLayout.java index 1e939b2fc4..ed5666a26c 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaLayout.java +++ b/src/java/org/apache/cassandra/locator/ReplicaLayout.java @@ -26,6 +26,8 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import java.util.Set; @@ -161,6 +163,7 @@ public abstract class ReplicaLayout> { this(replicationStrategy, natural, pending, null); } + public ForTokenWrite(AbstractReplicationStrategy replicationStrategy, EndpointsForToken natural, EndpointsForToken pending, EndpointsForToken all) { super(replicationStrategy, natural, pending, all); @@ -180,8 +183,7 @@ public abstract class ReplicaLayout> replicationStrategy(), natural().keep(filtered.endpoints()), pending().keep(filtered.endpoints()), - filtered - ); + filtered); } } @@ -202,13 +204,40 @@ public abstract class ReplicaLayout> * only responsibility is to fetch the 'natural' and 'pending' replicas, then resolve any conflicts * {@link ReplicaLayout#haveWriteConflicts(Endpoints, Endpoints)} */ - public static ReplicaLayout.ForTokenWrite forTokenWriteLiveAndDown(Keyspace keyspace, Token token) + public static ReplicaLayout.ForTokenWrite forTokenWriteLiveAndDown(Keyspace ks, Token token) { - // TODO: these should be cached, not the natural replicas - // TODO: race condition to fetch these. implications?? - AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForToken natural = EndpointsForToken.natural(replicationStrategy, token); - EndpointsForToken pending = EndpointsForToken.pending(keyspace, token); + return forTokenWriteLiveAndDown(ks.getMetadata(), token); + } + + public static ReplicaLayout.ForTokenWrite forTokenWriteLiveAndDown(KeyspaceMetadata ks, Token token) + { + ClusterMetadata metadata = ClusterMetadata.current(); + return forTokenWriteLiveAndDown(metadata, ks, token); + } + + // TODO: cleanup/remove Keyspace overloads + public static ReplicaLayout.ForTokenWrite forTokenWriteLiveAndDown(ClusterMetadata metadata, Keyspace ks, Token token) + { + return forTokenWriteLiveAndDown(metadata, ks.getMetadata(), token); + } + + public static ReplicaLayout.ForTokenWrite forTokenWriteLiveAndDown(ClusterMetadata metadata, KeyspaceMetadata ks, Token token) + { + AbstractReplicationStrategy replicationStrategy = ks.replicationStrategy; + EndpointsForToken natural; + EndpointsForToken pending; + if (ks.params.replication.isLocal()) + { + natural = forLocalStrategyToken(metadata, replicationStrategy, token); + pending = EndpointsForToken.empty(token); + } + else + { + // todo deduplicate so that "pending" contains "read - write", + // which is a hack until we revisit how consistency level handles pending + natural = forNonLocalStrategyTokenRead(metadata, ks, token); + pending = forNonLocalStrategyTokenWrite(metadata, ks, token).without(natural.endpoints()); + } return forTokenWrite(replicationStrategy, natural, pending); } @@ -326,9 +355,11 @@ public abstract class ReplicaLayout> * @return the read layout for a token - this includes only live natural replicas, i.e. those that are not pending * and not marked down by the failure detector. these are reverse sorted by the badness score of the configured snitch */ - public static ReplicaLayout.ForTokenRead forTokenReadLiveSorted(AbstractReplicationStrategy replicationStrategy, Token token) + static ReplicaLayout.ForTokenRead forTokenReadLiveSorted(ClusterMetadata metadata, Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, Token token) { - EndpointsForToken replicas = replicationStrategy.getNaturalReplicasForToken(token); + EndpointsForToken replicas = keyspace.getMetadata().params.replication.isLocal() + ? forLocalStrategyToken(metadata, replicationStrategy, token) + : forNonLocalStrategyTokenRead(metadata, keyspace.getMetadata(), token); replicas = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); replicas = replicas.filter(FailureDetector.isReplicaAlive); return new ReplicaLayout.ForTokenRead(replicationStrategy, replicas); @@ -339,11 +370,39 @@ public abstract class ReplicaLayout> * @return the read layout for a range - this includes only live natural replicas, i.e. those that are not pending * and not marked down by the failure detector. these are reverse sorted by the badness score of the configured snitch */ - static ReplicaLayout.ForRangeRead forRangeReadLiveSorted(AbstractReplicationStrategy replicationStrategy, AbstractBounds range) + static ReplicaLayout.ForRangeRead forRangeReadLiveSorted(ClusterMetadata metadata, Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, AbstractBounds range) { - EndpointsForRange replicas = replicationStrategy.getNaturalReplicas(range.right); + EndpointsForRange replicas = keyspace.getMetadata().params.replication.isLocal() + ? forLocalStrategyRange(metadata, replicationStrategy, range) + : forNonLocalStategyRangeRead(metadata, keyspace.getMetadata(), range); + replicas = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); replicas = replicas.filter(FailureDetector.isReplicaAlive); return new ReplicaLayout.ForRangeRead(replicationStrategy, range, replicas); } + + static EndpointsForRange forNonLocalStategyRangeRead(ClusterMetadata metadata, KeyspaceMetadata keyspace, AbstractBounds range) + { + return metadata.placements.get(keyspace.params.replication).reads.forRange(range.right.getToken()).get(); + } + + static EndpointsForToken forNonLocalStrategyTokenRead(ClusterMetadata metadata, KeyspaceMetadata keyspace, Token token) + { + return metadata.placements.get(keyspace.params.replication).reads.forToken(token).get(); + } + + static EndpointsForToken forNonLocalStrategyTokenWrite(ClusterMetadata metadata, KeyspaceMetadata keyspace, Token token) + { + return metadata.placements.get(keyspace.params.replication).writes.forToken(token).get(); + } + + static EndpointsForRange forLocalStrategyRange(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, AbstractBounds range) + { + return replicationStrategy.calculateNaturalReplicas(range.right.getToken(), metadata); + } + + static EndpointsForToken forLocalStrategyToken(ClusterMetadata metadata, AbstractReplicationStrategy replicationStrategy, Token t) + { + return replicationStrategy.calculateNaturalReplicas(t, metadata).forToken(t); + } } diff --git a/src/java/org/apache/cassandra/locator/ReplicaPlan.java b/src/java/org/apache/cassandra/locator/ReplicaPlan.java index 31dc2491fc..62db6f85f3 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaPlan.java +++ b/src/java/org/apache/cassandra/locator/ReplicaPlan.java @@ -23,12 +23,22 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.FBUtilities; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; public interface ReplicaPlan, P extends ReplicaPlan> { + Epoch epoch(); Keyspace keyspace(); AbstractReplicationStrategy replicationStrategy(); ConsistencyLevel consistencyLevel(); @@ -38,6 +48,10 @@ public interface ReplicaPlan, P extends ReplicaPlan Replica lookup(InetAddressAndPort endpoint); P withContacts(E contacts); + void collectSuccess(InetAddressAndPort inetAddressAndPort); + void collectFailure(InetAddressAndPort inetAddressAndPort, RequestFailureReason t); + boolean stillAppliesTo(ClusterMetadata newMetadata); + interface ForRead, P extends ReplicaPlan.ForRead> extends ReplicaPlan { int readQuorum(); @@ -57,6 +71,7 @@ public interface ReplicaPlan, P extends ReplicaPlan // It could be different than the one fetched from Keyspace later, e.g. RS altered during the query. // Use the snapshot to calculate {@code blockFor} in order to have a consistent view of RS for the query. protected final AbstractReplicationStrategy replicationStrategy; + protected final Epoch epoch; // all nodes we will contact via any mechanism, including hints // i.e., for: @@ -66,22 +81,43 @@ public interface ReplicaPlan, P extends ReplicaPlan // ==> liveAndDown.natural().filter(isFull) ++ liveAndDown.pending() ++ live.natural.filter(isTransient, req) // - paxos, includes all live replicas (natural+pending), for this DC if SERIAL_LOCAL // ==> live.all() (if consistencyLevel.isDCLocal(), then .filter(consistencyLevel.isLocal)) - private final E contacts; + protected final E contacts; - AbstractReplicaPlan(Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, E contacts) + protected final Function recompute; + protected List contacted = new CopyOnWriteArrayList<>(); + + AbstractReplicaPlan(Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, E contacts, Function recompute, Epoch epoch) { assert contacts != null; this.keyspace = keyspace; this.replicationStrategy = replicationStrategy; this.consistencyLevel = consistencyLevel; this.contacts = contacts; + this.recompute = recompute; + this.epoch = epoch; } public E contacts() { return contacts; } - public Keyspace keyspace() { return keyspace; } public AbstractReplicationStrategy replicationStrategy() { return replicationStrategy; } public ConsistencyLevel consistencyLevel() { return consistencyLevel; } + public boolean canDoLocalRequest() + { + return contacts.contains(FBUtilities.getBroadcastAddressAndPort()); + } + + public Epoch epoch() + { + return epoch; + } + + public void collectSuccess(InetAddressAndPort addr) + { + contacted.add(addr); + } + + public void collectFailure(InetAddressAndPort inetAddressAndPort, RequestFailureReason t) {} + } public static abstract class AbstractForRead, P extends ForRead> extends AbstractReplicaPlan implements ForRead @@ -89,14 +125,22 @@ public interface ReplicaPlan, P extends ReplicaPlan // all nodes we *could* contacts; typically all natural replicas that are believed to be alive // we will consult this collection to find uncontacted nodes we might contact if we doubt we will meet consistency level final E candidates; + final int readQuorum; - AbstractForRead(Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, E candidates, E contacts) + AbstractForRead(Keyspace keyspace, + AbstractReplicationStrategy replicationStrategy, + ConsistencyLevel consistencyLevel, + E candidates, + E contacts, + Function recompute, + Epoch epoch) { - super(keyspace, replicationStrategy, consistencyLevel, contacts); + super(keyspace, replicationStrategy, consistencyLevel, contacts, recompute, epoch); this.candidates = candidates; + this.readQuorum = consistencyLevel.blockFor(replicationStrategy); } - public int readQuorum() { return consistencyLevel.blockFor(replicationStrategy); } + public int readQuorum() { return readQuorum; } public E readCandidates() { return candidates; } @@ -114,27 +158,79 @@ public interface ReplicaPlan, P extends ReplicaPlan { return "ReplicaPlan.ForRead [ CL: " + consistencyLevel + " keyspace: " + keyspace + " candidates: " + candidates + " contacts: " + contacts() + " ]"; } + + @Override + public boolean stillAppliesTo(ClusterMetadata newMetadata) + { + if (newMetadata.epoch.equals(epoch)) + return true; + + // If we can't decide, return. + if (recompute == null) + return true; + + ForRead newPlan = recompute.apply(newMetadata); + + if (readCandidates().equals(newPlan.readCandidates())) + return true; + + int readQuorum = newPlan.readQuorum(); + for (InetAddressAndPort addr : contacted) + { + if (newPlan.readCandidates().contains(addr)) + readQuorum--; + } + + if (readQuorum <= 0) + return true; + + throw new IllegalStateException(String.format("During operation execution, for keyspace %s at %s the ring has changed from %s to %s in a way that would make responses violate the consistency level." + + "\n\tReceived responses from: %s" + + "\n\tOld candidates: %s" + + "\n\tNew candidates: %s" + + "\n\tRemaining required: %d", + keyspace.getName(), consistencyLevel, + epoch, newMetadata.epoch, + contacted, candidates, newPlan.readCandidates(), readQuorum)); + } } public static class ForTokenRead extends AbstractForRead { + private final Function, ForWrite> repairPlan; + public ForTokenRead(Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, EndpointsForToken candidates, - EndpointsForToken contacts) + EndpointsForToken contacts, + Function recompute, + Function, ReplicaPlan.ForWrite> repairPlan, + Epoch epoch) { - super(keyspace, replicationStrategy, consistencyLevel, candidates, contacts); + super(keyspace, replicationStrategy, consistencyLevel, candidates, contacts, recompute, epoch); + this.repairPlan = repairPlan; } - public ForTokenRead withContacts(EndpointsForToken newContact) + public ForTokenRead withContacts(EndpointsForToken newContacts) { - return new ForTokenRead(keyspace, replicationStrategy, consistencyLevel, candidates, newContact); + ForTokenRead res = new ForTokenRead(keyspace, replicationStrategy, consistencyLevel, candidates, newContacts, recompute, repairPlan, epoch); + res.contacted.addAll(contacted); + return res; + } + + public ForWrite repairPlan() + { + if (repairPlan != null) + return repairPlan.apply(this); //.get(); //.withContacts(contacts); + + throw new IllegalStateException("Can not construct a repair plan on a derivative plan."); } } public static class ForRangeRead extends AbstractForRead { + private final BiFunction, Token, ReplicaPlan.ForWrite> repairPlan; final AbstractBounds range; final int vnodeCount; @@ -144,11 +240,15 @@ public interface ReplicaPlan, P extends ReplicaPlan AbstractBounds range, EndpointsForRange candidates, EndpointsForRange contact, - int vnodeCount) + int vnodeCount, + Function recompute, + BiFunction, Token, ReplicaPlan.ForWrite> repairPlan, + Epoch epoch) { - super(keyspace, replicationStrategy, consistencyLevel, candidates, contact); + super(keyspace, replicationStrategy, consistencyLevel, candidates, contact, recompute, epoch); this.range = range; this.vnodeCount = vnodeCount; + this.repairPlan = repairPlan; } public AbstractBounds range() { return range; } @@ -160,7 +260,19 @@ public interface ReplicaPlan, P extends ReplicaPlan public ForRangeRead withContacts(EndpointsForRange newContact) { - return new ForRangeRead(keyspace, replicationStrategy, consistencyLevel, range, readCandidates(), newContact, vnodeCount); + ForRangeRead res = new ForRangeRead(keyspace, replicationStrategy, consistencyLevel, range, readCandidates(), newContact, vnodeCount, recompute, repairPlan, epoch); + res.contacted.addAll(contacted); + return res; + } + + public ForWrite repairPlan(Token token) + { + if (repairPlan != null) + { + return repairPlan.apply(this, token); + } + + throw new IllegalStateException("Can not construct a repair plan on a derivative plan."); } } @@ -172,9 +284,14 @@ public interface ReplicaPlan, P extends ReplicaPlan AbstractBounds range, EndpointsForRange candidates, EndpointsForRange contact, - int vnodeCount) + int vnodeCount, + Epoch epoch) { - super(keyspace, replicationStrategy, consistencyLevel, range, candidates, contact, vnodeCount); + // A FullRangeRead plan, as part of a top K query, is not recomputed to check that it still applies should + // the epoch change during the course of query execution so no recomputation function is supplied. Likewise, + // no read repair is expected to be performed during this type of query so a null is also used in place of a + // function for calculating the repair plan. + super(keyspace, replicationStrategy, consistencyLevel, range, candidates, contact, vnodeCount, null, null, epoch); } @Override @@ -190,16 +307,26 @@ public interface ReplicaPlan, P extends ReplicaPlan final EndpointsForToken pending; final EndpointsForToken liveAndDown; final EndpointsForToken live; + final int writeQuorum; - public ForWrite(Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, EndpointsForToken pending, EndpointsForToken liveAndDown, EndpointsForToken live, EndpointsForToken contact) + public ForWrite(Keyspace keyspace, + AbstractReplicationStrategy replicationStrategy, + ConsistencyLevel consistencyLevel, + EndpointsForToken pending, + EndpointsForToken liveAndDown, + EndpointsForToken live, + EndpointsForToken contact, + Function recompute, + Epoch epoch) { - super(keyspace, replicationStrategy, consistencyLevel, contact); + super(keyspace, replicationStrategy, consistencyLevel, contact, recompute, epoch); this.pending = pending; this.liveAndDown = liveAndDown; this.live = live; + this.writeQuorum = consistencyLevel.blockForWrite(replicationStrategy, pending); } - public int writeQuorum() { return consistencyLevel.blockForWrite(replicationStrategy, pending()); } + public int writeQuorum() { return writeQuorum; } /** Replicas that a region of the ring is moving to; not yet ready to serve reads, but should receive writes */ public EndpointsForToken pending() { return pending; } @@ -223,12 +350,56 @@ public interface ReplicaPlan, P extends ReplicaPlan private ForWrite copy(ConsistencyLevel newConsistencyLevel, EndpointsForToken newContact) { - return new ForWrite(keyspace, replicationStrategy, newConsistencyLevel, pending(), liveAndDown(), live(), newContact); + ForWrite res = new ForWrite(keyspace, replicationStrategy, newConsistencyLevel, pending(), liveAndDown(), live(), newContact, recompute, epoch); + res.contacted.addAll(contacted); + return res; } ForWrite withConsistencyLevel(ConsistencyLevel newConsistencylevel) { return copy(newConsistencylevel, contacts()); } public ForWrite withContacts(EndpointsForToken newContact) { return copy(consistencyLevel, newContact); } + // TODO: this method can return a collection of received responses that apply, and an explanation on why + // contacts are not enough to satisfy the replicaplan. + public boolean stillAppliesTo(ClusterMetadata newMetadata) + { + if (newMetadata.epoch.equals(epoch)) + return true; + + // If we can't decide, return. + if (recompute == null) + return true; + + ForWrite newPlan = recompute.apply(newMetadata); + + // We do not concern ourselves with down nodes here, at least not if we could make a successful write on them + if (liveAndDown.equals(newPlan.liveAndDown) && pending.equals(newPlan.pending)) + return true; + + int writeQuorum = newPlan.writeQuorum(); + + for (InetAddressAndPort addr : contacted) + { + if (newPlan.liveAndDown().contains(addr)) + writeQuorum--; + } + + if (writeQuorum <= 0) + return true; + + throw new IllegalStateException(String.format("During operation execution, for keyspace %s at %s the ring has changed from %s to %s in a way that would make responses violate the consistency level." + + "\n\tReceived responses from: %s" + + "\n\tOld candidates: %s%s" + + "\n\tNew candidates: %s%s" + + "\n\tRemaining required: %d", + keyspace.getName(), + consistencyLevel, + epoch, newMetadata.epoch, + contacted, + liveAndDown, pending.isEmpty() ? "" : String.format(" (%s pending)", pending), + newPlan.liveAndDown, newPlan.pending.isEmpty() ? "" : String.format(" (%s pending)", newPlan.pending), + writeQuorum)); + } + public String toString() { return "ReplicaPlan.ForWrite [ CL: " + consistencyLevel + " keyspace: " + keyspace + " liveAndDown: " + liveAndDown + " live: " + live + " contacts: " + contacts() + " ]"; @@ -239,9 +410,17 @@ public interface ReplicaPlan, P extends ReplicaPlan { final int requiredParticipants; - ForPaxosWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, EndpointsForToken pending, EndpointsForToken liveAndDown, EndpointsForToken live, EndpointsForToken contact, int requiredParticipants) + ForPaxosWrite(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + EndpointsForToken pending, + EndpointsForToken liveAndDown, + EndpointsForToken live, + EndpointsForToken contact, + int requiredParticipants, + Function recompute, + Epoch epoch) { - super(keyspace, keyspace.getReplicationStrategy(), consistencyLevel, pending, liveAndDown, live, contact); + super(keyspace, keyspace.getReplicationStrategy(), consistencyLevel, pending, liveAndDown, live, contact, recompute, epoch); this.requiredParticipants = requiredParticipants; } diff --git a/src/java/org/apache/cassandra/locator/ReplicaPlans.java b/src/java/org/apache/cassandra/locator/ReplicaPlans.java index 5950cbec86..a63a33f50c 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaPlans.java +++ b/src/java/org/apache/cassandra/locator/ReplicaPlans.java @@ -42,9 +42,12 @@ import org.apache.cassandra.index.Index; import org.apache.cassandra.index.IndexStatusManager; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.service.reads.AlwaysSpeculativeRetryPolicy; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.utils.FBUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,9 +59,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.function.*; import javax.annotation.Nullable; @@ -115,10 +116,12 @@ public class ReplicaPlans { assureSufficientLiveReplicas(replicationStrategy, consistencyLevel, liveReplicas, consistencyLevel.blockFor(replicationStrategy), 1); } + static void assureSufficientLiveReplicasForWrite(AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, Endpoints allLive, Endpoints pendingWithDown) throws UnavailableException { assureSufficientLiveReplicas(replicationStrategy, consistencyLevel, allLive, consistencyLevel.blockForWrite(replicationStrategy, pendingWithDown), 0); } + static void assureSufficientLiveReplicas(AbstractReplicationStrategy replicationStrategy, ConsistencyLevel consistencyLevel, Endpoints allLive, int blockFor, int blockForFullReplicas) throws UnavailableException { switch (consistencyLevel) @@ -183,34 +186,85 @@ public class ReplicaPlans /** * Construct a ReplicaPlan for writing to exactly one node, with CL.ONE. This node is *assumed* to be alive. */ - public static ReplicaPlan.ForWrite forSingleReplicaWrite(Keyspace keyspace, Token token, Replica replica) + public static ReplicaPlan.ForWrite forSingleReplicaWrite(ClusterMetadata metadata, Keyspace keyspace, Token token, Function replicaSupplier) { - EndpointsForToken one = EndpointsForToken.of(token, replica); + EndpointsForToken one = EndpointsForToken.of(token, replicaSupplier.apply(metadata)); EndpointsForToken empty = EndpointsForToken.empty(token); - return new ReplicaPlan.ForWrite(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, empty, one, one, one); + + return new ReplicaPlan.ForWrite(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, empty, one, one, one, + (newClusterMetadata) -> forSingleReplicaWrite(newClusterMetadata, keyspace, token, replicaSupplier), + metadata.epoch); + } + + /** + * Find a suitable replica as leader for counter update. + * For now, we pick a random replica in the local DC (or ask the snitch if + * there is no replica alive in the local DC). + * + * TODO: if we track the latency of the counter writes (which makes sense + * contrarily to standard writes since there is a read involved), we could + * trust the dynamic snitch entirely, which may be a better solution. It + * is unclear we want to mix those latencies with read latencies, so this + * may be a bit involved. + */ + public static Replica findCounterLeaderReplica(ClusterMetadata metadata, String keyspaceName, DecoratedKey key, String localDataCenter, ConsistencyLevel cl) throws UnavailableException + { + Keyspace keyspace = Keyspace.open(keyspaceName); + IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); + AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); + + EndpointsForToken replicas = metadata.placements.get(keyspace.getMetadata().params.replication).reads.forToken(key.getToken()).get(); + + // CASSANDRA-13043: filter out those endpoints not accepting clients yet, maybe because still bootstrapping + // TODO: replace this with JOINED state. + // TODO don't forget adding replicas = replicas.filter(replica -> FailureDetector.instance.isAlive(replica.endpoint())); after rebase (from CASSANDRA-17411) + replicas = replicas.filter(replica -> StorageService.instance.isRpcReady(replica.endpoint())); + + // TODO have a way to compute the consistency level + if (replicas.isEmpty()) + throw UnavailableException.create(cl, cl.blockFor(replicationStrategy), 0); + + List localReplicas = new ArrayList<>(replicas.size()); + + for (Replica replica : replicas) + if (snitch.getDatacenter(replica).equals(localDataCenter)) + localReplicas.add(replica); + + if (localReplicas.isEmpty()) + { + // If the consistency required is local then we should not involve other DCs + if (cl.isDatacenterLocal()) + throw UnavailableException.create(cl, cl.blockFor(replicationStrategy), 0); + + // No endpoint in local DC, pick the closest endpoint according to the snitch + replicas = snitch.sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); + return replicas.get(0); + } + + return localReplicas.get(ThreadLocalRandom.current().nextInt(localReplicas.size())); } /** * A forwarding counter write is always sent to a single owning coordinator for the range, by the original coordinator * (if it is not itself an owner) */ - public static ReplicaPlan.ForWrite forForwardingCounterWrite(Keyspace keyspace, Token token, Replica replica) + public static ReplicaPlan.ForWrite forForwardingCounterWrite(ClusterMetadata metadata, Keyspace keyspace, Token token, Function replica) { - return forSingleReplicaWrite(keyspace, token, replica); + return forSingleReplicaWrite(metadata, keyspace, token, replica); } public static ReplicaPlan.ForWrite forLocalBatchlogWrite() { Token token = DatabaseDescriptor.getPartitioner().getMinimumToken(); - Keyspace systemKeypsace = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME); + Keyspace systemKeyspace = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME); Replica localSystemReplica = SystemReplicas.getSystemReplica(FBUtilities.getBroadcastAddressAndPort()); ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWrite( - systemKeypsace.getReplicationStrategy(), - EndpointsForToken.of(token, localSystemReplica), - EndpointsForToken.empty(token) + systemKeyspace.getReplicationStrategy(), + EndpointsForToken.of(token, localSystemReplica), + EndpointsForToken.empty(token) ); - return forWrite(systemKeypsace, ConsistencyLevel.ONE, liveAndDown, liveAndDown, writeAll); + return forWrite(systemKeyspace, ConsistencyLevel.ONE, (cm) -> liveAndDown, (cm) -> true, writeAll); } /** @@ -221,43 +275,64 @@ public class ReplicaPlans */ public static ReplicaPlan.ForWrite forBatchlogWrite(boolean isAny) throws UnavailableException { - // A single case we write not for range or token, but multiple mutations to many tokens - Token token = DatabaseDescriptor.getPartitioner().getMinimumToken(); + return forBatchlogWrite(ClusterMetadata.current(), isAny); + } - TokenMetadata.Topology topology = StorageService.instance.getTokenMetadata().cachedOnlyTokenMap().getTopology(); + private static ReplicaLayout.ForTokenWrite liveAndDownForBatchlogWrite(Token token, ClusterMetadata metadata, boolean isAny) + { IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - Multimap localEndpoints = HashMultimap.create(topology.getDatacenterRacks() + Multimap localEndpoints = HashMultimap.create(metadata.directory.allDatacenterRacks() .get(snitch.getLocalDatacenter())); // Replicas are picked manually: // - replicas should be alive according to the failure detector // - replicas should be in the local datacenter // - choose min(2, number of qualifying candiates above) // - allow the local node to be the only replica only if it's a single-node DC - Collection chosenEndpoints = filterBatchlogEndpoints(snitch.getLocalRack(), localEndpoints); + Collection chosenEndpoints = filterBatchlogEndpoints(snitch.getLocalRack(), + localEndpoints, + Collections::shuffle, + (r) -> FailureDetector.isEndpointAlive.test(r) && metadata.directory.peerState(r) == NodeState.JOINED, + ThreadLocalRandom.current()::nextInt); if (chosenEndpoints.isEmpty() && isAny) chosenEndpoints = Collections.singleton(FBUtilities.getBroadcastAddressAndPort()); - Keyspace systemKeypsace = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME); - ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWrite( - systemKeypsace.getReplicationStrategy(), - SystemReplicas.getSystemReplicas(chosenEndpoints).forToken(token), - EndpointsForToken.empty(token) - ); - // Batchlog is hosted by either one node or two nodes from different racks. - ConsistencyLevel consistencyLevel = liveAndDown.all().size() == 1 ? ConsistencyLevel.ONE : ConsistencyLevel.TWO; - // assume that we have already been given live endpoints, and skip applying the failure detector - return forWrite(systemKeypsace, consistencyLevel, liveAndDown, liveAndDown, writeAll); + return ReplicaLayout.forTokenWrite(Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getReplicationStrategy(), + SystemReplicas.getSystemReplicas(chosenEndpoints).forToken(token), + EndpointsForToken.empty(token)); } - private static Collection filterBatchlogEndpoints(String localRack, - Multimap endpoints) + public static ReplicaPlan.ForWrite forBatchlogWrite(ClusterMetadata metadata, boolean isAny) throws UnavailableException { - return filterBatchlogEndpoints(localRack, - endpoints, - Collections::shuffle, - FailureDetector.isEndpointAlive, - ThreadLocalRandom.current()::nextInt); + // A single case we write not for range or token, but multiple mutations to many tokens + Token token = DatabaseDescriptor.getPartitioner().getMinimumToken(); + Keyspace systemKeyspace = Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME); + + ReplicaLayout.ForTokenWrite liveAndDown = liveAndDownForBatchlogWrite(token, metadata, isAny); + // Batchlog is hosted by either one node or two nodes from different racks. + ConsistencyLevel consistencyLevel = liveAndDown.all().size() == 1 ? ConsistencyLevel.ONE : ConsistencyLevel.TWO; + + AbstractReplicationStrategy replicationStrategy = liveAndDown.replicationStrategy(); + EndpointsForToken contacts = writeAll.select(consistencyLevel, liveAndDown, liveAndDown); + assureSufficientLiveReplicasForWrite(replicationStrategy, consistencyLevel, liveAndDown.all(), liveAndDown.pending()); + return new ReplicaPlan.ForWrite(systemKeyspace, + replicationStrategy, + consistencyLevel, + liveAndDown.pending(), + liveAndDown.all(), + liveAndDown.all().filter(FailureDetector.isReplicaAlive), + contacts, + (newMetadata) -> forBatchlogWrite(newMetadata, isAny), + metadata.epoch) { + @Override + public boolean stillAppliesTo(ClusterMetadata newMetadata) + { + if (liveAndDown.stream().allMatch(r -> newMetadata.directory.peerState(r.endpoint()) == NodeState.JOINED)) + return true; + + return super.stillAppliesTo(newMetadata); + } + }; } // Collect a list of candidates for batchlog hosting. If possible these will be two nodes from different racks. @@ -265,7 +340,7 @@ public class ReplicaPlans public static Collection filterBatchlogEndpoints(String localRack, Multimap endpoints, Consumer> shuffle, - Predicate isAlive, + Predicate include, Function indexPicker) { // special case for single-node data centers @@ -277,7 +352,7 @@ public class ReplicaPlans for (Map.Entry entry : endpoints.entries()) { InetAddressAndPort addr = entry.getValue(); - if (!addr.equals(FBUtilities.getBroadcastAddressAndPort()) && isAlive.test(addr)) + if (!addr.equals(FBUtilities.getBroadcastAddressAndPort()) && include.test(addr)) validated.put(entry.getKey(), entry.getValue()); } @@ -325,41 +400,76 @@ public class ReplicaPlans return result; } - public static ReplicaPlan.ForWrite forReadRepair(Token token, ReplicaPlan readPlan) throws UnavailableException + public static ReplicaPlan.ForWrite forReadRepair(ReplicaPlan forRead, ClusterMetadata metadata, Keyspace keyspace, ConsistencyLevel consistencyLevel, Token token, Predicate isAlive) throws UnavailableException { - return forWrite(readPlan.keyspace(), readPlan.consistencyLevel(), token, writeReadRepair(readPlan)); + AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); + Selector selector = writeReadRepair(forRead); + + ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace, token); + ReplicaLayout.ForTokenWrite live = liveAndDown.filter(isAlive); + + EndpointsForToken contacts = selector.select(consistencyLevel, liveAndDown, live); + assureSufficientLiveReplicasForWrite(replicationStrategy, consistencyLevel, live.all(), liveAndDown.pending()); + return new ReplicaPlan.ForWrite(keyspace, + replicationStrategy, + consistencyLevel, + liveAndDown.pending(), + liveAndDown.all(), + live.all(), + contacts, + (newClusterMetadata) -> forReadRepair(forRead, newClusterMetadata, keyspace, consistencyLevel, token, isAlive), + metadata.epoch); } public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, Token token, Selector selector) throws UnavailableException { - return forWrite(keyspace, consistencyLevel, ReplicaLayout.forTokenWriteLiveAndDown(keyspace, token), selector); + return forWrite(ClusterMetadata.current(), keyspace, consistencyLevel, token, selector); + } + + public static ReplicaPlan.ForWrite forWrite(ClusterMetadata metadata, Keyspace keyspace, ConsistencyLevel consistencyLevel, Token token, Selector selector) throws UnavailableException + { + return forWrite(metadata, keyspace, consistencyLevel, (newClusterMetadata) -> ReplicaLayout.forTokenWriteLiveAndDown(newClusterMetadata, keyspace, token), selector); } @VisibleForTesting - public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, EndpointsForToken natural, EndpointsForToken pending, Predicate isAlive, Selector selector) throws UnavailableException + public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, Function natural, Function pending, Epoch lastModified, Predicate isAlive, Selector selector) throws UnavailableException { - return forWrite(keyspace, consistencyLevel, ReplicaLayout.forTokenWrite(keyspace.getReplicationStrategy(), natural, pending), isAlive, selector); + return forWrite(keyspace, consistencyLevel, (newClusterMetadata) -> ReplicaLayout.forTokenWrite(keyspace.getReplicationStrategy(), natural.apply(newClusterMetadata), pending.apply(newClusterMetadata)), isAlive, selector); } - public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaLayout.ForTokenWrite liveAndDown, Selector selector) throws UnavailableException + public static ReplicaPlan.ForWrite forWrite(ClusterMetadata metadata, Keyspace keyspace, ConsistencyLevel consistencyLevel, Function liveAndDown, Selector selector) throws UnavailableException { - return forWrite(keyspace, consistencyLevel, liveAndDown, FailureDetector.isReplicaAlive, selector); + return forWrite(metadata, keyspace, consistencyLevel, liveAndDown, FailureDetector.isReplicaAlive, selector); } - private static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaLayout.ForTokenWrite liveAndDown, Predicate isAlive, Selector selector) throws UnavailableException + public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, Function liveAndDownSupplier, Predicate isAlive, Selector selector) throws UnavailableException { + return forWrite(ClusterMetadata.current(), keyspace, consistencyLevel, liveAndDownSupplier, isAlive, selector); + } + + public static ReplicaPlan.ForWrite forWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function liveAndDownSupplier, + Predicate isAlive, + Selector selector) throws UnavailableException + { + ReplicaLayout.ForTokenWrite liveAndDown = liveAndDownSupplier.apply(metadata); ReplicaLayout.ForTokenWrite live = liveAndDown.filter(isAlive); - return forWrite(keyspace, consistencyLevel, liveAndDown, live, selector); - } - public static ReplicaPlan.ForWrite forWrite(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaLayout.ForTokenWrite liveAndDown, ReplicaLayout.ForTokenWrite live, Selector selector) throws UnavailableException - { - assert liveAndDown.replicationStrategy() == live.replicationStrategy() - : "ReplicaLayout liveAndDown and live should be derived from the same replication strategy."; AbstractReplicationStrategy replicationStrategy = liveAndDown.replicationStrategy(); EndpointsForToken contacts = selector.select(consistencyLevel, liveAndDown, live); assureSufficientLiveReplicasForWrite(replicationStrategy, consistencyLevel, live.all(), liveAndDown.pending()); - return new ReplicaPlan.ForWrite(keyspace, replicationStrategy, consistencyLevel, liveAndDown.pending(), liveAndDown.all(), live.all(), contacts); + + return new ReplicaPlan.ForWrite(keyspace, + replicationStrategy, + consistencyLevel, + liveAndDown.pending(), + liveAndDown.all(), + live.all(), + contacts, + (newClusterMetadata) -> forWrite(newClusterMetadata, keyspace, consistencyLevel, liveAndDownSupplier, isAlive, selector), + metadata.epoch); } public interface Selector @@ -494,10 +604,15 @@ public class ReplicaPlans * This will select all live nodes as the candidates for the operation. Only the required number of participants */ public static ReplicaPlan.ForPaxosWrite forPaxos(Keyspace keyspace, DecoratedKey key, ConsistencyLevel consistencyForPaxos) throws UnavailableException + { + return forPaxos(ClusterMetadata.current(), keyspace, key, consistencyForPaxos, true); + } + + public static ReplicaPlan.ForPaxosWrite forPaxos(ClusterMetadata metadata, Keyspace keyspace, DecoratedKey key, ConsistencyLevel consistencyForPaxos, boolean throwOnInsufficientLiveReplicas) throws UnavailableException { Token tk = key.getToken(); - ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(keyspace, tk); + ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace, tk); Replicas.temporaryAssertFull(liveAndDown.all()); // TODO CASSANDRA-14547 @@ -514,20 +629,30 @@ public class ReplicaPlans int participants = liveAndDown.all().size(); int requiredParticipants = participants / 2 + 1; // See CASSANDRA-8346, CASSANDRA-833 - EndpointsForToken contacts = live.all(); - if (contacts.size() < requiredParticipants) - throw UnavailableException.create(consistencyForPaxos, requiredParticipants, contacts.size()); + if (throwOnInsufficientLiveReplicas) + { + if (live.all().size() < requiredParticipants) + throw UnavailableException.create(consistencyForPaxos, requiredParticipants, live.all().size()); - // We cannot allow CAS operations with 2 or more pending endpoints, see #8346. - // Note that we fake an impossible number of required nodes in the unavailable exception - // to nail home the point that it's an impossible operation no matter how many nodes are live. - if (liveAndDown.pending().size() > 1) - throw new UnavailableException(String.format("Cannot perform LWT operation as there is more than one (%d) pending range movement", liveAndDown.all().size()), - consistencyForPaxos, - participants + 1, - contacts.size()); + // We cannot allow CAS operations with 2 or more pending endpoints, see #8346. + // Note that we fake an impossible number of required nodes in the unavailable exception + // to nail home the point that it's an impossible operation no matter how many nodes are live. + if (liveAndDown.pending().size() > 1) + throw new UnavailableException(String.format("Cannot perform LWT operation as there is more than one (%d) pending range movement", liveAndDown.all().size()), + consistencyForPaxos, + participants + 1, + live.all().size()); + } - return new ReplicaPlan.ForPaxosWrite(keyspace, consistencyForPaxos, liveAndDown.pending(), liveAndDown.all(), live.all(), contacts, requiredParticipants); + return new ReplicaPlan.ForPaxosWrite(keyspace, + consistencyForPaxos, + liveAndDown.pending(), + liveAndDown.all(), + live.all(), + live.all(), + requiredParticipants, + (newClusterMetadata) -> forPaxos(newClusterMetadata, keyspace, key, consistencyForPaxos, false), + metadata.epoch); } private static > E candidatesForRead(Keyspace keyspace, @@ -575,18 +700,44 @@ public class ReplicaPlans */ public static ReplicaPlan.ForTokenRead forSingleReplicaRead(Keyspace keyspace, Token token, Replica replica) { + return forSingleReplicaRead(ClusterMetadata.current(), keyspace, token, replica); + } + + private static ReplicaPlan.ForTokenRead forSingleReplicaRead(ClusterMetadata metadata, Keyspace keyspace, Token token, Replica replica) + { + // todo; replica does not always contain token, figure out why +// if (!metadata.placements.get(keyspace.getMetadata().params.replication).reads.forToken(token).contains(replica)) +// throw UnavailableException.create(ConsistencyLevel.ONE, 1, 1, 0, 0); + EndpointsForToken one = EndpointsForToken.of(token, replica); - return new ReplicaPlan.ForTokenRead(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, one, one); + + return new ReplicaPlan.ForTokenRead(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, one, one, + (newClusterMetadata) -> forSingleReplicaRead(newClusterMetadata, keyspace, token, replica), + (self) -> { + throw new IllegalStateException("Read repair is not supported for short read/replica filtering protection."); + }, + metadata.epoch); } /** * Construct a plan for reading from a single node - this permits no speculation or read-repair */ public static ReplicaPlan.ForRangeRead forSingleReplicaRead(Keyspace keyspace, AbstractBounds range, Replica replica, int vnodeCount) + { + return forSingleReplicaRead(ClusterMetadata.current(), keyspace, range, replica, vnodeCount); + } + + private static ReplicaPlan.ForRangeRead forSingleReplicaRead(ClusterMetadata metadata, Keyspace keyspace, AbstractBounds range, Replica replica, int vnodeCount) { // TODO: this is unsafe, as one.range() may be inconsistent with our supplied range; should refactor Range/AbstractBounds to single class EndpointsForRange one = EndpointsForRange.of(replica); - return new ReplicaPlan.ForRangeRead(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, range, one, one, vnodeCount); + + return new ReplicaPlan.ForRangeRead(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, range, one, one, vnodeCount, + (newClusterMetadata) -> forSingleReplicaRead(metadata, keyspace, range, replica, vnodeCount), + (self, token) -> { + throw new IllegalStateException("Read repair is not supported for short read/replica filtering protection."); + }, + metadata.epoch); } /** @@ -602,13 +753,34 @@ public class ReplicaPlans @Nullable Index.QueryPlan indexQueryPlan, ConsistencyLevel consistencyLevel, SpeculativeRetryPolicy retry) + { + return forRead(ClusterMetadata.current(), keyspace, token, indexQueryPlan, consistencyLevel, retry, false); + } + + public static ReplicaPlan.ForTokenRead forRead(ClusterMetadata metadata, + Keyspace keyspace, + Token token, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + SpeculativeRetryPolicy retry) + { + return forRead(metadata, keyspace, token, indexQueryPlan, consistencyLevel, retry, true); + } + + private static ReplicaPlan.ForTokenRead forRead(ClusterMetadata metadata, Keyspace keyspace, Token token, @Nullable Index.QueryPlan indexQueryPlan, ConsistencyLevel consistencyLevel, SpeculativeRetryPolicy retry, boolean throwOnInsufficientLiveReplicas) { AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForToken candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forTokenReadLiveSorted(replicationStrategy, token).natural()); + ReplicaLayout.ForTokenRead forTokenRead = ReplicaLayout.forTokenReadLiveSorted(metadata, keyspace, replicationStrategy, token); + EndpointsForToken candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, forTokenRead.natural()); EndpointsForToken contacts = contactForRead(replicationStrategy, consistencyLevel, retry.equals(AlwaysSpeculativeRetryPolicy.INSTANCE), candidates); - assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); - return new ReplicaPlan.ForTokenRead(keyspace, replicationStrategy, consistencyLevel, candidates, contacts); + if (throwOnInsufficientLiveReplicas) + assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); + + return new ReplicaPlan.ForTokenRead(keyspace, replicationStrategy, consistencyLevel, candidates, contacts, + (newClusterMetadata) -> forRead(newClusterMetadata, keyspace, token, indexQueryPlan, consistencyLevel, retry, false), + (self) -> forReadRepair(self, metadata, keyspace, consistencyLevel, token, FailureDetector.isReplicaAlive), + metadata.epoch); } /** @@ -623,13 +795,36 @@ public class ReplicaPlans ConsistencyLevel consistencyLevel, AbstractBounds range, int vnodeCount) + { + return forRangeRead(ClusterMetadata.current(), keyspace, indexQueryPlan, consistencyLevel, range, vnodeCount, true); + } + + public static ReplicaPlan.ForRangeRead forRangeRead(ClusterMetadata metadata, + Keyspace keyspace, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + AbstractBounds range, + int vnodeCount, + boolean throwOnInsufficientLiveReplicas) { AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForRange candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, ReplicaLayout.forRangeReadLiveSorted(replicationStrategy, range).natural()); + ReplicaLayout.ForRangeRead forRangeRead = ReplicaLayout.forRangeReadLiveSorted(metadata, keyspace, replicationStrategy, range); + EndpointsForRange candidates = candidatesForRead(keyspace, indexQueryPlan, consistencyLevel, forRangeRead.natural()); EndpointsForRange contacts = contactForRead(replicationStrategy, consistencyLevel, false, candidates); - assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); - return new ReplicaPlan.ForRangeRead(keyspace, replicationStrategy, consistencyLevel, range, candidates, contacts, vnodeCount); + if (throwOnInsufficientLiveReplicas) + assureSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, contacts); + + return new ReplicaPlan.ForRangeRead(keyspace, + replicationStrategy, + consistencyLevel, + range, + candidates, + contacts, + vnodeCount, + (newClusterMetadata) -> forRangeRead(newClusterMetadata, keyspace, indexQueryPlan, consistencyLevel, range, vnodeCount, false), + (self, token) -> forReadRepair(self, metadata, keyspace, consistencyLevel, token, FailureDetector.isReplicaAlive), + metadata.epoch); } /** @@ -640,7 +835,11 @@ public class ReplicaPlans * - endpoints should be alive and satifies consistency requirement. * - each endpoint will be considered as replica of entire token ring, so coordinator can execute request with given range */ - public static ReplicaPlan.ForRangeRead forFullRangeRead(Keyspace keyspace, ConsistencyLevel consistencyLevel, AbstractBounds range, Set endpointsToContact, int vnodeCount) + public static ReplicaPlan.ForRangeRead forFullRangeRead(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + AbstractBounds range, + Set endpointsToContact, + int vnodeCount) { AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); EndpointsForRange.Builder builder = EndpointsForRange.Builder.builder(FULL_TOKEN_RANGE); @@ -649,30 +848,59 @@ public class ReplicaPlans EndpointsForRange contacts = builder.build(); - return new ReplicaPlan.ForFullRangeRead(keyspace, replicationStrategy, consistencyLevel, range, contacts, contacts, vnodeCount); + ClusterMetadata metadata = ClusterMetadata.current(); + return new ReplicaPlan.ForFullRangeRead(keyspace, replicationStrategy, consistencyLevel, range, contacts, contacts, vnodeCount, metadata.epoch); } /** * Take two range read plans for adjacent ranges, and check if it is OK (and worthwhile) to combine them into a single plan */ - public static ReplicaPlan.ForRangeRead maybeMerge(Keyspace keyspace, ConsistencyLevel consistencyLevel, ReplicaPlan.ForRangeRead left, ReplicaPlan.ForRangeRead right) + public static ReplicaPlan.ForRangeRead maybeMerge(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + ReplicaPlan.ForRangeRead left, + ReplicaPlan.ForRangeRead right) { - // TODO: should we be asserting that the ranges are adjacent? - AbstractBounds newRange = left.range().withNewRight(right.range().right); - EndpointsForRange mergedCandidates = left.readCandidates().keep(right.readCandidates().endpoints()); - AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); + assert left.range.right.equals(right.range.left); - // Check if there are enough shared endpoints for the merge to be possible. - if (!isSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, mergedCandidates)) + if (!left.epoch.equals(right.epoch)) return null; + EndpointsForRange mergedCandidates = left.readCandidates().keep(right.readCandidates().endpoints()); + AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); EndpointsForRange contacts = contactForRead(replicationStrategy, consistencyLevel, false, mergedCandidates); // Estimate whether merging will be a win or not if (!DatabaseDescriptor.getEndpointSnitch().isWorthMergingForRangeQuery(contacts, left.contacts(), right.contacts())) return null; + AbstractBounds newRange = left.range().withNewRight(right.range().right); + + // Check if there are enough shared endpoints for the merge to be possible. + if (!isSufficientLiveReplicasForRead(replicationStrategy, consistencyLevel, mergedCandidates)) + return null; + + int newVnodeCount = left.vnodeCount() + right.vnodeCount(); + // If we get there, merge this range and the next one - return new ReplicaPlan.ForRangeRead(keyspace, replicationStrategy, consistencyLevel, newRange, mergedCandidates, contacts, left.vnodeCount() + right.vnodeCount()); + return new ReplicaPlan.ForRangeRead(keyspace, + replicationStrategy, + consistencyLevel, + newRange, + mergedCandidates, + contacts, + newVnodeCount, + (newClusterMetadata) -> forRangeRead(newClusterMetadata, + keyspace, + null, // TODO (TCM) - we only use the recomputed ForRangeRead to check stillAppliesTo - make sure passing null here is ok + consistencyLevel, + newRange, + newVnodeCount, + false), + (self, token) -> { + // It might happen that the ring has moved forward since the operation has started, but because we'll be recomputing a quorum + // after the operation is complete, we will catch inconsistencies either way. + return forReadRepair(self, ClusterMetadata.current(), keyspace, consistencyLevel, token, FailureDetector.isReplicaAlive); + }, + left.epoch); } } diff --git a/src/java/org/apache/cassandra/locator/SimpleStrategy.java b/src/java/org/apache/cassandra/locator/SimpleStrategy.java index 488b601ce7..1e15eab9be 100644 --- a/src/java/org/apache/cassandra/locator/SimpleStrategy.java +++ b/src/java/org/apache/cassandra/locator/SimpleStrategy.java @@ -17,11 +17,7 @@ */ package org.apache.cassandra.locator; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,7 +31,15 @@ import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.StorageService; - +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.TokenMap; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; /** * This class returns the nodes responsible for a given @@ -45,28 +49,52 @@ import org.apache.cassandra.service.StorageService; */ public class SimpleStrategy extends AbstractReplicationStrategy { - private static final String REPLICATION_FACTOR = "replication_factor"; + public static final String REPLICATION_FACTOR = "replication_factor"; + private static final Logger logger = LoggerFactory.getLogger(SimpleStrategy.class); private final ReplicationFactor rf; - public SimpleStrategy(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map configOptions) + public SimpleStrategy(String keyspaceName, Map configOptions) { - super(keyspaceName, tokenMetadata, snitch, configOptions); + super(keyspaceName, configOptions); validateOptionsInternal(configOptions); this.rf = ReplicationFactor.fromString(this.configOptions.get(REPLICATION_FACTOR)); } - @Override - public EndpointsForRange calculateNaturalReplicas(Token token, TokenMetadata metadata) - { - ArrayList ring = metadata.sortedTokens(); - if (ring.isEmpty()) - return EndpointsForRange.empty(new Range<>(metadata.partitioner.getMinimumToken(), metadata.partitioner.getMinimumToken())); - Token replicaEnd = TokenMetadata.firstToken(ring, token); - Token replicaStart = metadata.getPredecessor(replicaEnd); - Range replicaRange = new Range<>(replicaStart, replicaEnd); - Iterator iter = TokenMetadata.ringIterator(ring, token, false); + @Override + public DataPlacement calculateDataPlacement(Epoch epoch, List> ranges, ClusterMetadata metadata) + { + PlacementForRange.Builder builder = PlacementForRange.builder(); + for (Range range : ranges) + builder.withReplicaGroup(VersionedEndpoints.forRange(epoch, + calculateNaturalReplicas(range.right, metadata.tokenMap.tokens(), range, metadata.directory, metadata.tokenMap))); + + PlacementForRange built = builder.build(); + return new DataPlacement(built, built); + } + + @Override + public EndpointsForRange calculateNaturalReplicas(Token token, ClusterMetadata metadata) + { + List ring = metadata.tokenMap.tokens(); + if (ring.isEmpty()) + return EndpointsForRange.empty(new Range<>(metadata.tokenMap.partitioner().getMinimumToken(), metadata.tokenMap.partitioner().getMinimumToken())); + + Range replicaRange = TokenRingUtils.getRange(ring, token); + return calculateNaturalReplicas(token, ring, replicaRange, metadata.directory, metadata.tokenMap); + } + + private EndpointsForRange calculateNaturalReplicas(Token token, + List ring, + Range replicaRange, + Directory endpoints, + TokenMap tokens) + { + if (ring.isEmpty()) + return EndpointsForRange.empty(new Range<>(tokens.partitioner().getMinimumToken(), token.getPartitioner().getMinimumToken())); + + Iterator iter = TokenRingUtils.ringIterator(ring, token, false); EndpointsForRange.Builder replicas = new EndpointsForRange.Builder(replicaRange, rf.allReplicas); @@ -74,7 +102,8 @@ public class SimpleStrategy extends AbstractReplicationStrategy while (replicas.size() < rf.allReplicas && iter.hasNext()) { Token tk = iter.next(); - InetAddressAndPort ep = metadata.getEndpoint(tk); + NodeId owner = tokens.owner(tk); + InetAddressAndPort ep = endpoints.endpoint(owner); if (!replicas.endpoints().contains(ep)) replicas.add(new Replica(ep, replicaRange, replicas.size() < rf.fullReplicas)); } @@ -82,6 +111,7 @@ public class SimpleStrategy extends AbstractReplicationStrategy return replicas.build(); } + @Override public ReplicationFactor getReplicationFactor() { @@ -124,7 +154,7 @@ public class SimpleStrategy extends AbstractReplicationStrategy } @Override - public Collection recognizedOptions() + public Collection recognizedOptions(ClusterMetadata metadata) { return Collections.singleton(REPLICATION_FACTOR); } diff --git a/src/java/org/apache/cassandra/locator/TokenMetadataDiagnostics.java b/src/java/org/apache/cassandra/locator/SystemStrategy.java similarity index 52% rename from src/java/org/apache/cassandra/locator/TokenMetadataDiagnostics.java rename to src/java/org/apache/cassandra/locator/SystemStrategy.java index 0221f1e22f..fa8ebf0c66 100644 --- a/src/java/org/apache/cassandra/locator/TokenMetadataDiagnostics.java +++ b/src/java/org/apache/cassandra/locator/SystemStrategy.java @@ -18,29 +18,36 @@ package org.apache.cassandra.locator; -import org.apache.cassandra.diag.DiagnosticEventService; -import org.apache.cassandra.locator.TokenMetadataEvent.TokenMetadataEventType; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.tcm.ClusterMetadata; /** - * Utility methods for events related to {@link TokenMetadata} changes. + * A replication strategy that is uniform for all tokens and ignores TokenMetadata */ -final class TokenMetadataDiagnostics +public abstract class SystemStrategy extends AbstractReplicationStrategy { - private static final DiagnosticEventService service = DiagnosticEventService.instance(); + public SystemStrategy(String keyspaceName, Map configOptions) + { + super(keyspaceName, configOptions); + } - private TokenMetadataDiagnostics() + public void validateOptions() throws ConfigurationException { } - static void pendingRangeCalculationStarted(TokenMetadata tokenMetadata, String keyspace) + @Override + public void maybeWarnOnOptions() { - if (isEnabled(TokenMetadataEventType.PENDING_RANGE_CALCULATION_STARTED)) - service.publish(new TokenMetadataEvent(TokenMetadataEventType.PENDING_RANGE_CALCULATION_STARTED, tokenMetadata, keyspace)); } - private static boolean isEnabled(TokenMetadataEventType type) + @Override + public Collection recognizedOptions(ClusterMetadata metadata) { - return service.isEnabled(TokenMetadataEvent.class, type); + // SystemStrategy doesn't expect any options. + return Collections.emptySet(); } - -} +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/locator/TokenMetadata.java b/src/java/org/apache/cassandra/locator/TokenMetadata.java deleted file mode 100644 index 151674a10d..0000000000 --- a/src/java/org/apache/cassandra/locator/TokenMetadata.java +++ /dev/null @@ -1,1611 +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.locator; - -import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import javax.annotation.concurrent.GuardedBy; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.*; - -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.DecoratedKey; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.FailureDetector; -import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.BiMultiValMap; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.SortedBiMultiValMap; - -import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; - -public class TokenMetadata -{ - private static final Logger logger = LoggerFactory.getLogger(TokenMetadata.class); - - /** - * Maintains token to endpoint map of every node in the cluster. - * Each Token is associated with exactly one Address, but each Address may have - * multiple tokens. Hence, the BiMultiValMap collection. - */ - private final BiMultiValMap tokenToEndpointMap; - - /** Maintains endpoint to host ID map of every node in the cluster */ - private final BiMap endpointToHostIdMap; - - // Prior to CASSANDRA-603, we just had Map pendingRanges, - // which was added to when a node began bootstrap and removed from when it finished. - // - // This is inadequate when multiple changes are allowed simultaneously. For example, - // suppose that there is a ring of nodes A, C and E, with replication factor 3. - // Node D bootstraps between C and E, so its pending ranges will be E-A, A-C and C-D. - // Now suppose node B bootstraps between A and C at the same time. Its pending ranges - // would be C-E, E-A and A-B. Now both nodes need to be assigned pending range E-A, - // which we would be unable to represent with the old Map. The same thing happens - // even more obviously for any nodes that boot simultaneously between same two nodes. - // - // So, we made two changes: - // - // First, we changed pendingRanges to a Multimap (now - // Map>, because replication strategy - // and options are per-KeySpace). - // - // Second, we added the bootstrapTokens and leavingEndpoints collections, so we can - // rebuild pendingRanges from the complete information of what is going on, when - // additional changes are made mid-operation. - // - // Finally, note that recording the tokens of joining nodes in bootstrapTokens also - // means we can detect and reject the addition of multiple nodes at the same token - // before one becomes part of the ring. - private final BiMultiValMap bootstrapTokens = new BiMultiValMap<>(); - - private final BiMap replacementToOriginal = HashBiMap.create(); - - // (don't need to record Token here since it's still part of tokenToEndpointMap until it's done leaving) - private final Set leavingEndpoints = new HashSet<>(); - // this is a cache of the calculation from {tokenToEndpointMap, bootstrapTokens, leavingEndpoints} - // NOTE: this may contain ranges that conflict with the those implied by sortedTokens when a range is changing its transient status - private final ConcurrentMap pendingRanges = new ConcurrentHashMap(); - - // nodes which are migrating to the new tokens in the ring - private final Set> movingEndpoints = new HashSet<>(); - - /* Use this lock for manipulating the token map */ - private final ReadWriteLock lock = new ReentrantReadWriteLock(true); - private volatile ArrayList sortedTokens; // safe to be read without a lock, as it's never mutated - - private volatile Topology topology; - - public final IPartitioner partitioner; - - // signals replication strategies that nodes have joined or left the ring and they need to recompute ownership - @GuardedBy("lock") - private long ringVersion = 0; - - public TokenMetadata() - { - this(SortedBiMultiValMap.create(), - HashBiMap.create(), - Topology.empty(), - DatabaseDescriptor.getPartitioner()); - } - - public TokenMetadata(IEndpointSnitch snitch) - { - this(SortedBiMultiValMap.create(), - HashBiMap.create(), - Topology.builder(() -> snitch).build(), - DatabaseDescriptor.getPartitioner()); - } - - private TokenMetadata(BiMultiValMap tokenToEndpointMap, BiMap endpointsMap, Topology topology, IPartitioner partitioner) - { - this(tokenToEndpointMap, endpointsMap, topology, partitioner, 0); - } - - private TokenMetadata(BiMultiValMap tokenToEndpointMap, BiMap endpointsMap, Topology topology, IPartitioner partitioner, long ringVersion) - { - this.tokenToEndpointMap = tokenToEndpointMap; - this.topology = topology; - this.partitioner = partitioner; - endpointToHostIdMap = endpointsMap; - sortedTokens = sortTokens(); - this.ringVersion = ringVersion; - } - - /** - * To be used by tests only (via {@link org.apache.cassandra.service.StorageService#setPartitionerUnsafe}). - */ - @VisibleForTesting - public TokenMetadata cloneWithNewPartitioner(IPartitioner newPartitioner) - { - return new TokenMetadata(tokenToEndpointMap, endpointToHostIdMap, topology, newPartitioner); - } - - private ArrayList sortTokens() - { - return new ArrayList<>(tokenToEndpointMap.keySet()); - } - - /** @return the number of nodes bootstrapping into source's primary range */ - public int pendingRangeChanges(InetAddressAndPort source) - { - int n = 0; - Collection> sourceRanges = getPrimaryRangesFor(getTokens(source)); - lock.readLock().lock(); - try - { - for (Token token : bootstrapTokens.keySet()) - for (Range range : sourceRanges) - if (range.contains(token)) - n++; - } - finally - { - lock.readLock().unlock(); - } - return n; - } - - /** - * Update token map with a single token/endpoint pair in normal state. - */ - public void updateNormalToken(Token token, InetAddressAndPort endpoint) - { - updateNormalTokens(Collections.singleton(token), endpoint); - } - - public void updateNormalTokens(Collection tokens, InetAddressAndPort endpoint) - { - Multimap endpointTokens = HashMultimap.create(); - endpointTokens.putAll(endpoint, tokens); - updateNormalTokens(endpointTokens); - } - - /** - * Update token map with a set of token/endpoint pairs in normal state. - * - * Prefer this whenever there are multiple pairs to update, as each update (whether a single or multiple) - * is expensive (CASSANDRA-3831). - */ - public void updateNormalTokens(Multimap endpointTokens) - { - if (endpointTokens.isEmpty()) - return; - - lock.writeLock().lock(); - try - { - boolean shouldSortTokens = false; - Topology.Builder topologyBuilder = topology.unbuild(); - for (InetAddressAndPort endpoint : endpointTokens.keySet()) - { - Collection tokens = endpointTokens.get(endpoint); - - assert tokens != null && !tokens.isEmpty(); - - bootstrapTokens.removeValue(endpoint); - tokenToEndpointMap.removeValue(endpoint); - topologyBuilder.addEndpoint(endpoint); - leavingEndpoints.remove(endpoint); - replacementToOriginal.remove(endpoint); - removeFromMoving(endpoint); // also removing this endpoint from moving - - for (Token token : tokens) - { - InetAddressAndPort prev = tokenToEndpointMap.put(token, endpoint); - if (!endpoint.equals(prev)) - { - if (prev != null) - logger.warn("Token {} changing ownership from {} to {}", token, prev, endpoint); - shouldSortTokens = true; - } - } - } - topology = topologyBuilder.build(); - - if (shouldSortTokens) - sortedTokens = sortTokens(); - } - finally - { - lock.writeLock().unlock(); - } - } - - /** - * Store an end-point to host ID mapping. Each ID must be unique, and - * cannot be changed after the fact. - */ - public void updateHostId(UUID hostId, InetAddressAndPort endpoint) - { - assert hostId != null; - assert endpoint != null; - - lock.writeLock().lock(); - try - { - updateEndpointToHostIdMap(hostId, endpoint); - } - finally - { - lock.writeLock().unlock(); - } - - } - - public void updateHostIds(Map hostIdToEndpointMap) - { - lock.writeLock().lock(); - try - { - for (Map.Entry entry : hostIdToEndpointMap.entrySet()) - { - updateEndpointToHostIdMap(entry.getKey(), entry.getValue()); - } - } - finally - { - lock.writeLock().unlock(); - } - - } - - private void updateEndpointToHostIdMap(UUID hostId, InetAddressAndPort endpoint) - { - InetAddressAndPort storedEp = endpointToHostIdMap.inverse().get(hostId); - if (storedEp != null) - { - if (!storedEp.equals(endpoint) && (FailureDetector.instance.isAlive(storedEp))) - { - throw new RuntimeException(String.format("Host ID collision between active endpoint %s and %s (id=%s)", - storedEp, - endpoint, - hostId)); - } - } - - UUID storedId = endpointToHostIdMap.get(endpoint); - if ((storedId != null) && (!storedId.equals(hostId))) - logger.warn("Changing {}'s host ID from {} to {}", endpoint, storedId, hostId); - - endpointToHostIdMap.forcePut(endpoint, hostId); - } - - /** Return the unique host ID for an end-point. */ - public UUID getHostId(InetAddressAndPort endpoint) - { - lock.readLock().lock(); - try - { - return endpointToHostIdMap.get(endpoint); - } - finally - { - lock.readLock().unlock(); - } - } - - /** Return the end-point for a unique host ID */ - public InetAddressAndPort getEndpointForHostId(UUID hostId) - { - lock.readLock().lock(); - try - { - return endpointToHostIdMap.inverse().get(hostId); - } - finally - { - lock.readLock().unlock(); - } - } - - /** @return a copy of the endpoint-to-id map for read-only operations */ - public Map getEndpointToHostIdMapForReading() - { - lock.readLock().lock(); - try - { - Map readMap = new HashMap<>(); - readMap.putAll(endpointToHostIdMap); - return readMap; - } - finally - { - lock.readLock().unlock(); - } - } - - public void addBootstrapTokens(Collection tokens, InetAddressAndPort endpoint) - { - addBootstrapTokens(tokens, endpoint, null); - } - - private void addBootstrapTokens(Collection tokens, InetAddressAndPort endpoint, InetAddressAndPort original) - { - assert tokens != null && !tokens.isEmpty(); - assert endpoint != null; - - lock.writeLock().lock(); - try - { - - InetAddressAndPort oldEndpoint; - - for (Token token : tokens) - { - oldEndpoint = bootstrapTokens.get(token); - if (oldEndpoint != null && !oldEndpoint.equals(endpoint)) - throw new RuntimeException("Bootstrap Token collision between " + oldEndpoint + " and " + endpoint + " (token " + token); - - oldEndpoint = tokenToEndpointMap.get(token); - if (oldEndpoint != null && !oldEndpoint.equals(endpoint) && !oldEndpoint.equals(original)) - throw new RuntimeException("Bootstrap Token collision between " + oldEndpoint + " and " + endpoint + " (token " + token); - } - - bootstrapTokens.removeValue(endpoint); - - for (Token token : tokens) - bootstrapTokens.put(token, endpoint); - } - finally - { - lock.writeLock().unlock(); - } - } - - public void addReplaceTokens(Collection replacingTokens, InetAddressAndPort newNode, InetAddressAndPort oldNode) - { - assert replacingTokens != null && !replacingTokens.isEmpty(); - assert newNode != null && oldNode != null; - - lock.writeLock().lock(); - try - { - Collection oldNodeTokens = tokenToEndpointMap.inverse().get(oldNode); - if (!replacingTokens.containsAll(oldNodeTokens) || !oldNodeTokens.containsAll(replacingTokens)) - { - throw new RuntimeException(String.format("Node %s is trying to replace node %s with tokens %s with a " + - "different set of tokens %s.", newNode, oldNode, oldNodeTokens, - replacingTokens)); - } - - logger.debug("Replacing {} with {}", newNode, oldNode); - replacementToOriginal.put(newNode, oldNode); - - addBootstrapTokens(replacingTokens, newNode, oldNode); - } - finally - { - lock.writeLock().unlock(); - } - } - - public Optional getReplacementNode(InetAddressAndPort endpoint) - { - lock.readLock().lock(); - try - { - return Optional.ofNullable(replacementToOriginal.inverse().get(endpoint)); - } - finally - { - lock.readLock().unlock(); - } - } - - public Optional getReplacingNode(InetAddressAndPort endpoint) - { - lock.readLock().lock(); - try - { - return Optional.ofNullable((replacementToOriginal.get(endpoint))); - } - finally - { - lock.readLock().unlock(); - } - } - - public void removeBootstrapTokens(Collection tokens) - { - assert tokens != null && !tokens.isEmpty(); - - lock.writeLock().lock(); - try - { - for (Token token : tokens) - bootstrapTokens.remove(token); - } - finally - { - lock.writeLock().unlock(); - } - } - - public void addLeavingEndpoint(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.writeLock().lock(); - try - { - leavingEndpoints.add(endpoint); - } - finally - { - lock.writeLock().unlock(); - } - } - - /** - * Add a new moving endpoint - * @param token token which is node moving to - * @param endpoint address of the moving node - */ - public void addMovingEndpoint(Token token, InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.writeLock().lock(); - try - { - movingEndpoints.add(Pair.create(token, endpoint)); - } - finally - { - lock.writeLock().unlock(); - } - } - - public void removeEndpoint(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.writeLock().lock(); - try - { - bootstrapTokens.removeValue(endpoint); - - topology = topology.unbuild().removeEndpoint(endpoint).build(); - leavingEndpoints.remove(endpoint); - if (replacementToOriginal.remove(endpoint) != null) - { - logger.debug("Node {} failed during replace.", endpoint); - } - endpointToHostIdMap.remove(endpoint); - Collection removedTokens = tokenToEndpointMap.removeValue(endpoint); - if (removedTokens != null && !removedTokens.isEmpty()) - { - sortedTokens = sortTokens(); - invalidateCachedRingsUnsafe(); - } - } - finally - { - lock.writeLock().unlock(); - } - } - - /** - * This is called when the snitch properties for this endpoint are updated, see CASSANDRA-10238. - */ - public Topology updateTopology(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.writeLock().lock(); - try - { - logger.info("Updating topology for {}", endpoint); - topology = topology.unbuild().updateEndpoint(endpoint).build(); - invalidateCachedRingsUnsafe(); - return topology; - } - finally - { - lock.writeLock().unlock(); - } - } - - /** - * This is called when the snitch properties for many endpoints are updated, it will update - * the topology mappings of any endpoints whose snitch has changed, see CASSANDRA-10238. - */ - public Topology updateTopology() - { - lock.writeLock().lock(); - try - { - logger.info("Updating topology for all endpoints that have changed"); - topology = topology.unbuild().updateEndpoints().build(); - invalidateCachedRingsUnsafe(); - return topology; - } - finally - { - lock.writeLock().unlock(); - } - } - - /** - * Remove pair of token/address from moving endpoints - * @param endpoint address of the moving node - */ - public void removeFromMoving(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.writeLock().lock(); - try - { - for (Pair pair : movingEndpoints) - { - if (pair.right.equals(endpoint)) - { - movingEndpoints.remove(pair); - break; - } - } - - invalidateCachedRingsUnsafe(); - } - finally - { - lock.writeLock().unlock(); - } - } - - public Collection getTokens(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.readLock().lock(); - try - { - assert isMember(endpoint): String.format("Unable to get tokens for %s; it is not a member", endpoint); // don't want to return nulls - return new ArrayList<>(tokenToEndpointMap.inverse().get(endpoint)); - } - finally - { - lock.readLock().unlock(); - } - } - - public boolean isMember(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.readLock().lock(); - try - { - return tokenToEndpointMap.inverse().containsKey(endpoint); - } - finally - { - lock.readLock().unlock(); - } - } - - public boolean isLeaving(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.readLock().lock(); - try - { - return leavingEndpoints.contains(endpoint); - } - finally - { - lock.readLock().unlock(); - } - } - - public boolean isMoving(InetAddressAndPort endpoint) - { - assert endpoint != null; - - lock.readLock().lock(); - try - { - for (Pair pair : movingEndpoints) - { - if (pair.right.equals(endpoint)) - return true; - } - - return false; - } - finally - { - lock.readLock().unlock(); - } - } - - private final AtomicReference cachedTokenMap = new AtomicReference<>(); - - /** - * Create a copy of TokenMetadata with only tokenToEndpointMap. That is, pending ranges, - * bootstrap tokens and leaving endpoints are not included in the copy. - */ - public TokenMetadata cloneOnlyTokenMap() - { - lock.readLock().lock(); - try - { - return new TokenMetadata(SortedBiMultiValMap.create(tokenToEndpointMap), - HashBiMap.create(endpointToHostIdMap), - topology, - partitioner, - ringVersion); - } - finally - { - lock.readLock().unlock(); - } - } - - /** - * Return a cached TokenMetadata with only tokenToEndpointMap, i.e., the same as cloneOnlyTokenMap but - * uses a cached copy that is invalided when the ring changes, so in the common case - * no extra locking is required. - * - * Callers must *NOT* mutate the returned metadata object. - */ - public TokenMetadata cachedOnlyTokenMap() - { - TokenMetadata tm = cachedTokenMap.get(); - if (tm != null) - return tm; - - // synchronize to prevent thundering herd (CASSANDRA-6345) - synchronized (this) - { - if ((tm = cachedTokenMap.get()) != null) - return tm; - - tm = cloneOnlyTokenMap(); - cachedTokenMap.set(tm); - return tm; - } - } - - /** - * Create a copy of TokenMetadata with tokenToEndpointMap reflecting situation after all - * current leave operations have finished. - * - * @return new token metadata - */ - public TokenMetadata cloneAfterAllLeft() - { - lock.readLock().lock(); - try - { - return removeEndpoints(cloneOnlyTokenMap(), leavingEndpoints); - } - finally - { - lock.readLock().unlock(); - } - } - - private static TokenMetadata removeEndpoints(TokenMetadata allLeftMetadata, Set leavingEndpoints) - { - for (InetAddressAndPort endpoint : leavingEndpoints) - allLeftMetadata.removeEndpoint(endpoint); - - return allLeftMetadata; - } - - /** - * Create a copy of TokenMetadata with tokenToEndpointMap reflecting situation after all - * current leave, and move operations have finished. - * - * @return new token metadata - */ - public TokenMetadata cloneAfterAllSettled() - { - lock.readLock().lock(); - try - { - TokenMetadata metadata = cloneOnlyTokenMap(); - - for (InetAddressAndPort endpoint : leavingEndpoints) - metadata.removeEndpoint(endpoint); - - - for (Pair pair : movingEndpoints) - metadata.updateNormalToken(pair.left, pair.right); - - return metadata; - } - finally - { - lock.readLock().unlock(); - } - } - - public InetAddressAndPort getEndpoint(Token token) - { - lock.readLock().lock(); - try - { - return tokenToEndpointMap.get(token); - } - finally - { - lock.readLock().unlock(); - } - } - - public Collection> getPrimaryRangesFor(Collection tokens) - { - Collection> ranges = new ArrayList<>(tokens.size()); - for (Token right : tokens) - ranges.add(new Range<>(getPredecessor(right), right)); - return ranges; - } - - public ArrayList sortedTokens() - { - return sortedTokens; - } - - public EndpointsByRange getPendingRangesMM(String keyspaceName) - { - EndpointsByRange.Builder byRange = new EndpointsByRange.Builder(); - PendingRangeMaps pendingRangeMaps = this.pendingRanges.get(keyspaceName); - - if (pendingRangeMaps != null) - { - for (Map.Entry, EndpointsForRange.Builder> entry : pendingRangeMaps) - { - byRange.putAll(entry.getKey(), entry.getValue(), Conflict.ALL); - } - } - - return byRange.build(); - } - - /** a mutable map may be returned but caller should not modify it */ - public PendingRangeMaps getPendingRanges(String keyspaceName) - { - return this.pendingRanges.get(keyspaceName); - } - - public RangesAtEndpoint getPendingRanges(String keyspaceName, InetAddressAndPort endpoint) - { - RangesAtEndpoint.Builder builder = RangesAtEndpoint.builder(endpoint); - for (Map.Entry, Replica> entry : getPendingRangesMM(keyspaceName).flattenEntries()) - { - Replica replica = entry.getValue(); - if (replica.endpoint().equals(endpoint)) - { - builder.add(replica, Conflict.DUPLICATE); - } - } - return builder.build(); - } - - /** - * Calculate pending ranges according to bootsrapping and leaving nodes. Reasoning is: - * - * (1) When in doubt, it is better to write too much to a node than too little. That is, if - * there are multiple nodes moving, calculate the biggest ranges a node could have. Cleaning - * up unneeded data afterwards is better than missing writes during movement. - * (2) When a node leaves, ranges for other nodes can only grow (a node might get additional - * ranges, but it will not lose any of its current ranges as a result of a leave). Therefore - * we will first remove _all_ leaving tokens for the sake of calculation and then check what - * ranges would go where if all nodes are to leave. This way we get the biggest possible - * ranges with regard current leave operations, covering all subsets of possible final range - * values. - * (3) When a node bootstraps, ranges of other nodes can only get smaller. Without doing - * complex calculations to see if multiple bootstraps overlap, we simply base calculations - * on the same token ring used before (reflecting situation after all leave operations have - * completed). Bootstrapping nodes will be added and removed one by one to that metadata and - * checked what their ranges would be. This will give us the biggest possible ranges the - * node could have. It might be that other bootstraps make our actual final ranges smaller, - * but it does not matter as we can clean up the data afterwards. - * - * NOTE: This is heavy and ineffective operation. This will be done only once when a node - * changes state in the cluster, so it should be manageable. - */ - public void calculatePendingRanges(AbstractReplicationStrategy strategy, String keyspaceName) - { - // avoid race between both branches - do not use a lock here as this will block any other unrelated operations! - long startedAt = currentTimeMillis(); - synchronized (pendingRanges) - { - TokenMetadataDiagnostics.pendingRangeCalculationStarted(this, keyspaceName); - - unsafeCalculatePendingRanges(strategy, keyspaceName); - - if (logger.isDebugEnabled()) - logger.debug("Starting pending range calculation for {}", keyspaceName); - - long took = currentTimeMillis() - startedAt; - - if (logger.isDebugEnabled()) - logger.debug("Pending range calculation for {} completed (took: {}ms)", keyspaceName, took); - if (logger.isTraceEnabled()) - logger.trace("Calculated pending ranges for {}:\n{}", keyspaceName, (pendingRanges.isEmpty() ? "" : printPendingRanges())); - } - } - - public void unsafeCalculatePendingRanges(AbstractReplicationStrategy strategy, String keyspaceName) - { - // create clone of current state - BiMultiValMap bootstrapTokensClone; - Set leavingEndpointsClone; - Set> movingEndpointsClone; - TokenMetadata metadata; - - lock.readLock().lock(); - try - { - - if (bootstrapTokens.isEmpty() && leavingEndpoints.isEmpty() && movingEndpoints.isEmpty()) - { - if (logger.isTraceEnabled()) - logger.trace("No bootstrapping, leaving or moving nodes -> empty pending ranges for {}", keyspaceName); - if (bootstrapTokens.isEmpty() && leavingEndpoints.isEmpty() && movingEndpoints.isEmpty()) - { - if (logger.isTraceEnabled()) - logger.trace("No bootstrapping, leaving or moving nodes -> empty pending ranges for {}", keyspaceName); - pendingRanges.put(keyspaceName, new PendingRangeMaps()); - - return; - } - } - - bootstrapTokensClone = new BiMultiValMap<>(this.bootstrapTokens); - leavingEndpointsClone = new HashSet<>(this.leavingEndpoints); - movingEndpointsClone = new HashSet<>(this.movingEndpoints); - metadata = this.cloneOnlyTokenMap(); - } - finally - { - lock.readLock().unlock(); - } - - pendingRanges.put(keyspaceName, calculatePendingRanges(strategy, metadata, bootstrapTokensClone, - leavingEndpointsClone, movingEndpointsClone)); - } - - /** - * @see TokenMetadata#calculatePendingRanges(AbstractReplicationStrategy, String) - */ - private static PendingRangeMaps calculatePendingRanges(AbstractReplicationStrategy strategy, - TokenMetadata metadata, - BiMultiValMap bootstrapTokens, - Set leavingEndpoints, - Set> movingEndpoints) - { - PendingRangeMaps newPendingRanges = new PendingRangeMaps(); - - RangesByEndpoint addressRanges = strategy.getAddressReplicas(metadata); - - // Copy of metadata reflecting the situation after all leave operations are finished. - TokenMetadata allLeftMetadata = removeEndpoints(metadata.cloneOnlyTokenMap(), leavingEndpoints); - - // get all ranges that will be affected by leaving nodes - Set> removeAffectedRanges = new HashSet<>(); - for (InetAddressAndPort endpoint : leavingEndpoints) - removeAffectedRanges.addAll(addressRanges.get(endpoint).ranges()); - - // for each of those ranges, find what new nodes will be responsible for the range when - // all leaving nodes are gone. - for (Range range : removeAffectedRanges) - { - EndpointsForRange currentReplicas = strategy.calculateNaturalReplicas(range.right, metadata); - EndpointsForRange newReplicas = strategy.calculateNaturalReplicas(range.right, allLeftMetadata); - for (Replica newReplica : newReplicas) - { - if (currentReplicas.endpoints().contains(newReplica.endpoint())) - continue; - - // we calculate pending replicas for leave- and move- affected ranges in the same way to avoid - // a possible conflict when 2 pending replicas have the same endpoint and different ranges. - for (Replica pendingReplica : newReplica.subtractSameReplication(addressRanges.get(newReplica.endpoint()))) - newPendingRanges.addPendingRange(range, pendingReplica); - } - } - - // At this stage newPendingRanges has been updated according to leave operations. We can - // now continue the calculation by checking bootstrapping nodes. - - // For each of the bootstrapping nodes, simply add to the allLeftMetadata and check what their - // ranges would be. We actually need to clone allLeftMetadata each time as resetting its state - // after getting the new pending ranges is not as simple as just removing the bootstrapping - // endpoint. If the bootstrapping endpoint constitutes a replacement, removing it after checking - // the newly pending ranges means there are now fewer endpoints that there were originally and - // causes its next neighbour to take over its primary range which affects the next RF endpoints - // in the ring. - Multimap bootstrapAddresses = bootstrapTokens.inverse(); - for (InetAddressAndPort endpoint : bootstrapAddresses.keySet()) - { - Collection tokens = bootstrapAddresses.get(endpoint); - TokenMetadata cloned = allLeftMetadata.cloneOnlyTokenMap(); - cloned.updateNormalTokens(tokens, endpoint); - for (Replica replica : strategy.getAddressReplicas(cloned, endpoint)) - { - newPendingRanges.addPendingRange(replica.range(), replica); - } - } - - // At this stage newPendingRanges has been updated according to leaving and bootstrapping nodes. - // We can now finish the calculation by checking moving nodes. - - // For each of the moving nodes, we do the same thing we did for bootstrapping: - // simply add and remove them one by one to allLeftMetadata and check in between what their ranges would be. - for (Pair moving : movingEndpoints) - { - //Calculate all the ranges which will could be affected. This will include the ranges before and after the move. - Set moveAffectedReplicas = new HashSet<>(); - InetAddressAndPort endpoint = moving.right; // address of the moving node - //Add ranges before the move - for (Replica replica : strategy.getAddressReplicas(allLeftMetadata, endpoint)) - { - moveAffectedReplicas.add(replica); - } - - allLeftMetadata.updateNormalToken(moving.left, endpoint); - //Add ranges after the move - for (Replica replica : strategy.getAddressReplicas(allLeftMetadata, endpoint)) - { - moveAffectedReplicas.add(replica); - } - - for (Replica replica : moveAffectedReplicas) - { - Set currentEndpoints = strategy.calculateNaturalReplicas(replica.range().right, metadata).endpoints(); - Set newEndpoints = strategy.calculateNaturalReplicas(replica.range().right, allLeftMetadata).endpoints(); - Set difference = Sets.difference(newEndpoints, currentEndpoints); - for (final InetAddressAndPort address : difference) - { - RangesAtEndpoint newReplicas = strategy.getAddressReplicas(allLeftMetadata, address); - RangesAtEndpoint oldReplicas = strategy.getAddressReplicas(metadata, address); - - // Filter out the things that are already replicated - newReplicas = newReplicas.filter(r -> !oldReplicas.contains(r)); - for (Replica newReplica : newReplicas) - { - // for correctness on write, we need to treat ranges that are becoming full differently - // to those that are presently transient; however reads must continue to use the current view - // for ranges that are becoming transient. We could choose to ignore them here, but it's probably - // cleaner to ensure this is dealt with at point of use, where we can make a conscious decision - // about which to use - for (Replica pendingReplica : newReplica.subtractSameReplication(oldReplicas)) - { - newPendingRanges.addPendingRange(pendingReplica.range(), pendingReplica); - } - } - } - } - - allLeftMetadata.removeEndpoint(endpoint); - } - - return newPendingRanges; - } - - public Token getPredecessor(Token token) - { - List tokens = sortedTokens(); - int index = Collections.binarySearch(tokens, token); - assert index >= 0 : token + " not found in " + tokenToEndpointMapKeysAsStrings(); - return index == 0 ? tokens.get(tokens.size() - 1) : tokens.get(index - 1); - } - - public Token getSuccessor(Token token) - { - List tokens = sortedTokens(); - int index = Collections.binarySearch(tokens, token); - assert index >= 0 : token + " not found in " + tokenToEndpointMapKeysAsStrings(); - return (index == (tokens.size() - 1)) ? tokens.get(0) : tokens.get(index + 1); - } - - private String tokenToEndpointMapKeysAsStrings() - { - lock.readLock().lock(); - try - { - return StringUtils.join(tokenToEndpointMap.keySet(), ", "); - } - finally - { - lock.readLock().unlock(); - } - } - - /** @return a copy of the bootstrapping tokens map */ - public BiMultiValMap getBootstrapTokens() - { - lock.readLock().lock(); - try - { - return new BiMultiValMap<>(bootstrapTokens); - } - finally - { - lock.readLock().unlock(); - } - } - - public Set getAllEndpoints() - { - lock.readLock().lock(); - try - { - return ImmutableSet.copyOf(endpointToHostIdMap.keySet()); - } - finally - { - lock.readLock().unlock(); - } - } - - public int getSizeOfAllEndpoints() - { - lock.readLock().lock(); - try - { - return endpointToHostIdMap.size(); - } - finally - { - lock.readLock().unlock(); - } - } - - public Set getAllMembers() - { - return getAllEndpoints().stream() - .filter(this::isMember) - .collect(Collectors.toSet()); - } - - /** caller should not modify leavingEndpoints */ - public Set getLeavingEndpoints() - { - lock.readLock().lock(); - try - { - return ImmutableSet.copyOf(leavingEndpoints); - } - finally - { - lock.readLock().unlock(); - } - } - - public int getSizeOfLeavingEndpoints() - { - lock.readLock().lock(); - try - { - return leavingEndpoints.size(); - } - finally - { - lock.readLock().unlock(); - } - } - - /** - * Endpoints which are migrating to the new tokens - * @return set of addresses of moving endpoints - */ - public Set> getMovingEndpoints() - { - lock.readLock().lock(); - try - { - return ImmutableSet.copyOf(movingEndpoints); - } - finally - { - lock.readLock().unlock(); - } - } - - public int getSizeOfMovingEndpoints() - { - lock.readLock().lock(); - try - { - return movingEndpoints.size(); - } - finally - { - lock.readLock().unlock(); - } - } - - public static int firstTokenIndex(final ArrayList ring, Token start, boolean insertMin) - { - assert ring.size() > 0; - // insert the minimum token (at index == -1) if we were asked to include it and it isn't a member of the ring - int i = Collections.binarySearch(ring, start); - if (i < 0) - { - i = (i + 1) * (-1); - if (i >= ring.size()) - i = insertMin ? -1 : 0; - } - return i; - } - - public static Token firstToken(final ArrayList ring, Token start) - { - return ring.get(firstTokenIndex(ring, start, false)); - } - - /** - * iterator over the Tokens in the given ring, starting with the token for the node owning start - * (which does not have to be a Token in the ring) - * @param includeMin True if the minimum token should be returned in the ring even if it has no owner. - */ - public static Iterator ringIterator(final ArrayList ring, Token start, boolean includeMin) - { - if (ring.isEmpty()) - return includeMin ? Iterators.singletonIterator(start.getPartitioner().getMinimumToken()) - : Collections.emptyIterator(); - - final boolean insertMin = includeMin && !ring.get(0).isMinimum(); - final int startIndex = firstTokenIndex(ring, start, insertMin); - return new AbstractIterator() - { - int j = startIndex; - protected Token computeNext() - { - if (j < -1) - return endOfData(); - try - { - // return minimum for index == -1 - if (j == -1) - return start.getPartitioner().getMinimumToken(); - // return ring token for other indexes - return ring.get(j); - } - finally - { - j++; - if (j == ring.size()) - j = insertMin ? -1 : 0; - if (j == startIndex) - // end iteration - j = -2; - } - } - }; - } - - /** used by tests */ - public void clearUnsafe() - { - lock.writeLock().lock(); - try - { - tokenToEndpointMap.clear(); - endpointToHostIdMap.clear(); - bootstrapTokens.clear(); - leavingEndpoints.clear(); - pendingRanges.clear(); - movingEndpoints.clear(); - sortedTokens.clear(); - topology = Topology.empty(); - invalidateCachedRingsUnsafe(); - } - finally - { - lock.writeLock().unlock(); - } - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - lock.readLock().lock(); - try - { - Multimap endpointToTokenMap = tokenToEndpointMap.inverse(); - Set eps = endpointToTokenMap.keySet(); - - if (!eps.isEmpty()) - { - sb.append("Normal Tokens:"); - sb.append(LINE_SEPARATOR.getString()); - for (InetAddressAndPort ep : eps) - { - sb.append(ep); - sb.append(':'); - sb.append(endpointToTokenMap.get(ep)); - sb.append(LINE_SEPARATOR.getString()); - } - } - - if (!bootstrapTokens.isEmpty()) - { - sb.append("Bootstrapping Tokens:" ); - sb.append(LINE_SEPARATOR.getString()); - for (Map.Entry entry : bootstrapTokens.entrySet()) - { - sb.append(entry.getValue()).append(':').append(entry.getKey()); - sb.append(LINE_SEPARATOR.getString()); - } - } - - if (!leavingEndpoints.isEmpty()) - { - sb.append("Leaving Endpoints:"); - sb.append(LINE_SEPARATOR.getString()); - for (InetAddressAndPort ep : leavingEndpoints) - { - sb.append(ep); - sb.append(LINE_SEPARATOR.getString()); - } - } - - if (!pendingRanges.isEmpty()) - { - sb.append("Pending Ranges:"); - sb.append(LINE_SEPARATOR.getString()); - sb.append(printPendingRanges()); - } - } - finally - { - lock.readLock().unlock(); - } - - return sb.toString(); - } - - private String printPendingRanges() - { - StringBuilder sb = new StringBuilder(); - - for (PendingRangeMaps pendingRangeMaps : pendingRanges.values()) - { - sb.append(pendingRangeMaps.printPendingRanges()); - } - - return sb.toString(); - } - - public EndpointsForToken pendingEndpointsForToken(Token token, String keyspaceName) - { - PendingRangeMaps pendingRangeMaps = this.pendingRanges.get(keyspaceName); - if (pendingRangeMaps == null) - return EndpointsForToken.empty(token); - - return pendingRangeMaps.pendingEndpointsFor(token); - } - - /** - * @deprecated retained for benefit of old tests - */ - @Deprecated(since = "4.0") - public EndpointsForToken getWriteEndpoints(Token token, String keyspaceName, EndpointsForToken natural) - { - EndpointsForToken pending = pendingEndpointsForToken(token, keyspaceName); - return ReplicaLayout.forTokenWrite(Keyspace.open(keyspaceName).getReplicationStrategy(), natural, pending).all(); - } - - /** @return an endpoint to token multimap representation of tokenToEndpointMap (a copy) */ - public Multimap getEndpointToTokenMapForReading() - { - lock.readLock().lock(); - try - { - Multimap cloned = HashMultimap.create(); - for (Map.Entry entry : tokenToEndpointMap.entrySet()) - cloned.put(entry.getValue(), entry.getKey()); - return cloned; - } - finally - { - lock.readLock().unlock(); - } - } - - /** - * @return a (stable copy, won't be modified) Token to Endpoint map for all the normal and bootstrapping nodes - * in the cluster. - */ - public Map getNormalAndBootstrappingTokenToEndpointMap() - { - lock.readLock().lock(); - try - { - Map map = new HashMap<>(tokenToEndpointMap.size() + bootstrapTokens.size()); - map.putAll(tokenToEndpointMap); - map.putAll(bootstrapTokens); - return map; - } - finally - { - lock.readLock().unlock(); - } - } - - /** - * @return a (stable copy, won't be modified) datacenter to Endpoint map for all the nodes in the cluster. - */ - public ImmutableMultimap getDC2AllEndpoints(IEndpointSnitch snitch) - { - return Multimaps.index(getAllEndpoints(), snitch::getDatacenter); - } - - /** - * @return the Topology map of nodes to DCs + Racks - * - * This is only allowed when a copy has been made of TokenMetadata, to avoid concurrent modifications - * when Topology methods are subsequently used by the caller. - */ - public Topology getTopology() - { - assert !DatabaseDescriptor.isDaemonInitialized() || this != StorageService.instance.getTokenMetadata(); - return topology; - } - - public long getRingVersion() - { - lock.readLock().lock(); - - try - { - return ringVersion; - } - finally - { - lock.readLock().unlock(); - } - } - - public void invalidateCachedRings() - { - lock.writeLock().lock(); - - try - { - invalidateCachedRingsUnsafe(); - } - finally - { - lock.writeLock().unlock(); - } - } - - private void invalidateCachedRingsUnsafe() - { - ringVersion++; - cachedTokenMap.set(null); - } - - public DecoratedKey decorateKey(ByteBuffer key) - { - return partitioner.decorateKey(key); - } - - /** - * Tracks the assignment of racks and endpoints in each datacenter for all the "normal" endpoints - * in this TokenMetadata. This allows faster calculation of endpoints in NetworkTopologyStrategy. - */ - public static class Topology - { - /** multi-map of DC to endpoints in that DC */ - private final ImmutableMultimap dcEndpoints; - /** map of DC to multi-map of rack to endpoints in that rack */ - private final ImmutableMap> dcRacks; - /** reverse-lookup map for endpoint to current known dc/rack assignment */ - private final ImmutableMap> currentLocations; - private final Supplier snitchSupplier; - - private Topology(Builder builder) - { - this.dcEndpoints = ImmutableMultimap.copyOf(builder.dcEndpoints); - - ImmutableMap.Builder> dcRackBuilder = ImmutableMap.builder(); - for (Map.Entry> entry : builder.dcRacks.entrySet()) - dcRackBuilder.put(entry.getKey(), ImmutableMultimap.copyOf(entry.getValue())); - this.dcRacks = dcRackBuilder.build(); - - this.currentLocations = ImmutableMap.copyOf(builder.currentLocations); - this.snitchSupplier = builder.snitchSupplier; - } - - /** - * @return multi-map of DC to endpoints in that DC - */ - public Multimap getDatacenterEndpoints() - { - return dcEndpoints; - } - - /** - * @return map of DC to multi-map of rack to endpoints in that rack - */ - public ImmutableMap> getDatacenterRacks() - { - return dcRacks; - } - - /** - * @return The DC and rack of the given endpoint. - */ - public Pair getLocation(InetAddressAndPort addr) - { - return currentLocations.get(addr); - } - - Builder unbuild() - { - return new Builder(this); - } - - static Builder builder(Supplier snitchSupplier) - { - return new Builder(snitchSupplier); - } - - static Topology empty() - { - return builder(() -> DatabaseDescriptor.getEndpointSnitch()).build(); - } - - private static class Builder - { - /** multi-map of DC to endpoints in that DC */ - private final Multimap dcEndpoints; - /** map of DC to multi-map of rack to endpoints in that rack */ - private final Map> dcRacks; - /** reverse-lookup map for endpoint to current known dc/rack assignment */ - private final Map> currentLocations; - private final Supplier snitchSupplier; - - Builder(Supplier snitchSupplier) - { - this.dcEndpoints = HashMultimap.create(); - this.dcRacks = new HashMap<>(); - this.currentLocations = new HashMap<>(); - this.snitchSupplier = snitchSupplier; - } - - Builder(Topology from) - { - this.dcEndpoints = HashMultimap.create(from.dcEndpoints); - - this.dcRacks = Maps.newHashMapWithExpectedSize(from.dcRacks.size()); - for (Map.Entry> entry : from.dcRacks.entrySet()) - dcRacks.put(entry.getKey(), HashMultimap.create(entry.getValue())); - - this.currentLocations = new HashMap<>(from.currentLocations); - this.snitchSupplier = from.snitchSupplier; - } - - /** - * Stores current DC/rack assignment for ep - */ - Builder addEndpoint(InetAddressAndPort ep) - { - String dc = snitchSupplier.get().getDatacenter(ep); - String rack = snitchSupplier.get().getRack(ep); - Pair current = currentLocations.get(ep); - if (current != null) - { - if (current.left.equals(dc) && current.right.equals(rack)) - return this; - doRemoveEndpoint(ep, current); - } - - doAddEndpoint(ep, dc, rack); - return this; - } - - private void doAddEndpoint(InetAddressAndPort ep, String dc, String rack) - { - dcEndpoints.put(dc, ep); - - if (!dcRacks.containsKey(dc)) - dcRacks.put(dc, HashMultimap.create()); - dcRacks.get(dc).put(rack, ep); - - currentLocations.put(ep, Pair.create(dc, rack)); - } - - /** - * Removes current DC/rack assignment for ep - */ - Builder removeEndpoint(InetAddressAndPort ep) - { - if (!currentLocations.containsKey(ep)) - return this; - - doRemoveEndpoint(ep, currentLocations.remove(ep)); - return this; - } - - private void doRemoveEndpoint(InetAddressAndPort ep, Pair current) - { - dcRacks.get(current.left).remove(current.right, ep); - dcEndpoints.remove(current.left, ep); - } - - Builder updateEndpoint(InetAddressAndPort ep) - { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - if (snitch == null || !currentLocations.containsKey(ep)) - return this; - - updateEndpoint(ep, snitch); - return this; - } - - Builder updateEndpoints() - { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - if (snitch == null) - return this; - - for (InetAddressAndPort ep : currentLocations.keySet()) - updateEndpoint(ep, snitch); - - return this; - } - - private void updateEndpoint(InetAddressAndPort ep, IEndpointSnitch snitch) - { - Pair current = currentLocations.get(ep); - String dc = snitch.getDatacenter(ep); - String rack = snitch.getRack(ep); - if (dc.equals(current.left) && rack.equals(current.right)) - return; - - doRemoveEndpoint(ep, current); - doAddEndpoint(ep, dc, rack); - } - - Topology build() - { - return new Topology(this); - } - } - } -} diff --git a/src/java/org/apache/cassandra/locator/TokenMetadataEvent.java b/src/java/org/apache/cassandra/locator/TokenMetadataEvent.java deleted file mode 100644 index c3ed074b33..0000000000 --- a/src/java/org/apache/cassandra/locator/TokenMetadataEvent.java +++ /dev/null @@ -1,62 +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.locator; - -import java.io.Serializable; -import java.util.HashMap; - -import org.apache.cassandra.diag.DiagnosticEvent; - -/** - * Events related to {@link TokenMetadata} changes. - */ -public final class TokenMetadataEvent extends DiagnosticEvent -{ - - public enum TokenMetadataEventType - { - PENDING_RANGE_CALCULATION_STARTED, - PENDING_RANGE_CALCULATION_COMPLETED, - } - - private final TokenMetadataEventType type; - private final TokenMetadata tokenMetadata; - private final String keyspace; - - TokenMetadataEvent(TokenMetadataEventType type, TokenMetadata tokenMetadata, String keyspace) - { - this.type = type; - this.tokenMetadata = tokenMetadata; - this.keyspace = keyspace; - } - - public TokenMetadataEventType getType() - { - return type; - } - - public HashMap toMap() - { - // be extra defensive against nulls and bugs - HashMap ret = new HashMap<>(); - ret.put("keyspace", keyspace); - ret.put("tokenMetadata", tokenMetadata.toString()); - return ret; - } -} diff --git a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java index 707cca25e5..a6919885a1 100644 --- a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java +++ b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java @@ -130,6 +130,12 @@ public class KeyspaceMetrics public final Histogram bytesValidated; /** histogram over the number of partitions we have validated */ public final Histogram partitionsValidated; + /** Lifetime count of reads for keys outside the node's owned token ranges for this keyspace **/ + public final Counter outOfRangeTokenReads; + /** Lifetime count of writes for keys outside the node's owned token ranges for this keyspace **/ + public final Counter outOfRangeTokenWrites; + /** Lifetime count of paxos requests for keys outside the node's owned token ranges for this keyspace **/ + public final Counter outOfRangeTokenPaxosRequests; /* * Metrics for inconsistencies detected between repaired data sets across replicas. These @@ -276,6 +282,10 @@ public class KeyspaceMetrics rowIndexSize = createKeyspaceHistogram("RowIndexSize", false); formatSpecificGauges = createFormatSpecificGauges(keyspace); + + outOfRangeTokenReads = createKeyspaceCounter("ReadOutOfRangeToken"); + outOfRangeTokenWrites = createKeyspaceCounter("WriteOutOfRangeToken"); + outOfRangeTokenPaxosRequests = createKeyspaceCounter("PaxosOutOfRangeToken"); } /** diff --git a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java b/src/java/org/apache/cassandra/metrics/PaxosMetrics.java index 62c62d806e..1e1ba89ba2 100644 --- a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java +++ b/src/java/org/apache/cassandra/metrics/PaxosMetrics.java @@ -19,6 +19,7 @@ package org.apache.cassandra.metrics; import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; @@ -26,5 +27,7 @@ public class PaxosMetrics { private static final MetricNameFactory factory = new DefaultNameFactory("Paxos"); public static final Counter linearizabilityViolations = Metrics.counter(factory.createMetricName("LinearizabilityViolations")); + public static final Meter repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairPaxosTopologyRetries")); + public static void initialize() {} } diff --git a/src/java/org/apache/cassandra/metrics/StorageMetrics.java b/src/java/org/apache/cassandra/metrics/StorageMetrics.java index d86a2144f2..ebb031cc4d 100644 --- a/src/java/org/apache/cassandra/metrics/StorageMetrics.java +++ b/src/java/org/apache/cassandra/metrics/StorageMetrics.java @@ -45,6 +45,8 @@ public class StorageMetrics public static final Counter totalHintsInProgress = Metrics.counter(factory.createMetricName("TotalHintsInProgress")); public static final Counter totalHints = Metrics.counter(factory.createMetricName("TotalHints")); public static final Counter repairExceptions = Metrics.counter(factory.createMetricName("RepairExceptions")); + public static final Counter totalOpsForInvalidToken = Metrics.counter(factory.createMetricName("TotalOpsForInvalidToken")); + public static final Counter startupOpsForInvalidToken = Metrics.counter(factory.createMetricName("StartupOpsForInvalidToken")); private static Gauge createSummingGauge(String name, ToLongFunction extractor) { diff --git a/src/java/org/apache/cassandra/metrics/TCMMetrics.java b/src/java/org/apache/cassandra/metrics/TCMMetrics.java new file mode 100644 index 0000000000..61a3b9d1e9 --- /dev/null +++ b/src/java/org/apache/cassandra/metrics/TCMMetrics.java @@ -0,0 +1,154 @@ +/* + * 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.metrics; + +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.Timer; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics; + +public class TCMMetrics +{ + private static final MetricNameFactory factory = new DefaultNameFactory("TCM"); + + public static final TCMMetrics instance = new TCMMetrics(); + + public final Gauge currentEpochGauge; + public final Gauge currentCMSSize; + public final Gauge unreachableCMSMembers; + public final Gauge isCMSMember; + public final Histogram fetchedPeerLogEntries; + public final Histogram fetchedCMSLogEntries; + public final Timer fetchPeerLogLatency; + public final Timer fetchCMSLogLatency; + public final Meter fetchCMSLogConsistencyDowngrade; + public final Histogram servedPeerLogEntries; + public final Histogram servedCMSLogEntries; + public final Meter logEntryFetchRate; + public final Timer commitRejectionLatency; + public final Timer commitFailureLatency; + public final Timer commitSuccessLatency; + public final Meter commitRetries; + public final Meter fetchLogRetries; + public final Meter progressBarrierRetries; + // TODO: we eventually want to rely on (currently non-existing) metric that tracks paxos topology retries for all Paxos. + public final Meter repairPaxosTopologyRetries; + public final Timer progressBarrierLatency; + public final Meter progressBarrierCLRelax; + public final Meter coordinatorBehindSchema; + public final Meter coordinatorBehindPlacements; + + private TCMMetrics() + { + currentEpochGauge = Metrics.register(factory.createMetricName("Epoch"), () -> { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + return Epoch.EMPTY.getEpoch(); + return metadata.epoch.getEpoch(); + }); + + currentCMSSize = Metrics.register(factory.createMetricName("CMSSize"), () -> { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + return 0L; + return (long)metadata.fullCMSMembers().size(); + }); + + unreachableCMSMembers = Metrics.register(factory.createMetricName("UnreachableCMSMembers"), () -> { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + return 0L; + return metadata.fullCMSMembers().stream().filter(FailureDetector.isEndpointAlive.negate()).count(); + }); + + isCMSMember = Metrics.register(factory.createMetricName("IsCMSMember"), () -> { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + return metadata != null && metadata.isCMSMember(FBUtilities.getBroadcastAddressAndPort()) ? 1 : 0; + }); + + fetchedPeerLogEntries = Metrics.histogram(factory.createMetricName("FetchedPeerLogEntries"), false); + fetchPeerLogLatency = Metrics.timer(factory.createMetricName("FetchPeerLogLatency")); + fetchedCMSLogEntries = Metrics.histogram(factory.createMetricName("FetchedCMSLogEntries"), false); + fetchCMSLogLatency = Metrics.timer(factory.createMetricName("FetchCMSLogLatency")); + fetchCMSLogConsistencyDowngrade = Metrics.meter(factory.createMetricName("FetchCMSLogConsistencyDowngradeRelax")); + servedPeerLogEntries = Metrics.histogram(factory.createMetricName("ServedPeerLogEntries"), false); + servedCMSLogEntries = Metrics.histogram(factory.createMetricName("ServedCMSLogEntries"), false); + fetchLogRetries = Metrics.meter(factory.createMetricName("FetchLogRetries")); + repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairCMSPaxosTopologyRetries")); + logEntryFetchRate = Metrics.meter(factory.createMetricName("LogEntryFetchRate")); + + commitRejectionLatency = Metrics.timer(factory.createMetricName("CommitRejectionLatency")); + commitFailureLatency = Metrics.timer(factory.createMetricName("CommitFailureLatency")); + commitSuccessLatency = Metrics.timer(factory.createMetricName("CommitSuccessLatency")); + commitRetries = Metrics.meter(factory.createMetricName("CommitRetries")); + + progressBarrierRetries = Metrics.meter(factory.createMetricName("ProgressBarrierRetries")); + progressBarrierLatency = Metrics.timer(factory.createMetricName("ProgressBarrierLatency")); + progressBarrierCLRelax = Metrics.meter(factory.createMetricName("ProgressBarrierCLRelaxed")); + + coordinatorBehindSchema = Metrics.meter(factory.createMetricName("CoordinatorBehindSchema")); + coordinatorBehindPlacements = Metrics.meter(factory.createMetricName("CoordinatorBehindPlacements")); + } + + public void recordCommitFailureLatency(long latency, TimeUnit timeUnit, boolean isRejection) + { + if (isRejection) + commitRejectionLatency.update(latency, timeUnit); + else + commitFailureLatency.update(latency, timeUnit); + } + + public void peerLogEntriesFetched(Epoch before, Epoch after) + { + logEntryFetchRate.mark(); + updateLogEntryHistogram(fetchedPeerLogEntries, before, after); + } + + public void cmsLogEntriesFetched(Epoch before, Epoch after) + { + logEntryFetchRate.mark(); + updateLogEntryHistogram(fetchedCMSLogEntries, before, after); + } + + public void peerLogEntriesServed(Epoch before, Epoch after) + { + updateLogEntryHistogram(servedPeerLogEntries, before, after); + } + + public void cmsLogEntriesServed(Epoch before, Epoch after) + { + updateLogEntryHistogram(servedCMSLogEntries, before, after); + } + + private void updateLogEntryHistogram(Histogram histogram, Epoch before, Epoch after) + { + if (after.isAfter(before)) + histogram.update(after.getEpoch() - before.getEpoch()); + else + histogram.update(0L); + } +} diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java index b02cf1237e..8d78625ea8 100644 --- a/src/java/org/apache/cassandra/metrics/TableMetrics.java +++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java @@ -289,6 +289,8 @@ public class TableMetrics Keyspace k = Schema.instance.getKeyspaceInstance(keyspace); if (SchemaConstants.DISTRIBUTED_KEYSPACE_NAME.equals(k.getName())) continue; + if (k.getMetadata().params.replication.isMeta()) + continue; if (k.getReplicationStrategy().getReplicationFactor().allReplicas < 2) continue; diff --git a/src/java/org/apache/cassandra/net/InboundMessageHandler.java b/src/java/org/apache/cassandra/net/InboundMessageHandler.java index 5fb7fdcb93..ce75b67656 100644 --- a/src/java/org/apache/cassandra/net/InboundMessageHandler.java +++ b/src/java/org/apache/cassandra/net/InboundMessageHandler.java @@ -37,6 +37,7 @@ import org.apache.cassandra.net.Message.Header; import org.apache.cassandra.net.FrameDecoder.IntactFrame; import org.apache.cassandra.net.FrameDecoder.CorruptFrame; import org.apache.cassandra.net.ResourceLimits.Limit; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tracing.TraceState; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -125,7 +126,7 @@ public class InboundMessageHandler extends AbstractMessageHandler long currentTimeNanos = approxTime.now(); Header header = serializer.extractHeader(buf, peer, currentTimeNanos, version); long timeElapsed = currentTimeNanos - header.createdAtNanos; - int size = serializer.inferMessageSize(buf, buf.position(), buf.limit()); + int size = serializer.inferMessageSize(buf, buf.position(), buf.limit(), version); if (approxTime.isAfter(currentTimeNanos, header.expiresAtNanos)) { @@ -172,6 +173,7 @@ public class InboundMessageHandler extends AbstractMessageHandler { callbacks.onFailedDeserialize(size, header, e); noSpamLogger.info("{} incompatible schema encountered while deserializing a message", this, e); + ClusterMetadataService.instance().fetchLogFromPeerAsync(header.from, header.epoch); } catch (Throwable t) { @@ -211,7 +213,7 @@ public class InboundMessageHandler extends AbstractMessageHandler long currentTimeNanos = approxTime.now(); Header header = serializer.extractHeader(buf, peer, currentTimeNanos, version); - int size = serializer.inferMessageSize(buf, buf.position(), buf.limit()); + int size = serializer.inferMessageSize(buf, buf.position(), buf.limit(), version); boolean expired = approxTime.isAfter(currentTimeNanos, header.expiresAtNanos); if (!expired && !acquireCapacity(endpointReserve, globalReserve, size, currentTimeNanos, header.expiresAtNanos)) diff --git a/src/java/org/apache/cassandra/net/InboundSink.java b/src/java/org/apache/cassandra/net/InboundSink.java index 9d68ba1aa0..d077039635 100644 --- a/src/java/org/apache/cassandra/net/InboundSink.java +++ b/src/java/org/apache/cassandra/net/InboundSink.java @@ -18,6 +18,7 @@ package org.apache.cassandra.net; import java.io.IOException; +import java.util.EnumSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Predicate; @@ -26,9 +27,14 @@ import org.slf4j.LoggerFactory; import net.openhft.chronicle.core.util.ThrowingConsumer; import org.apache.cassandra.db.filter.TombstoneOverwhelmingException; +import org.apache.cassandra.exceptions.CoordinatorBehindException; +import org.apache.cassandra.exceptions.InvalidRoutingException; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.index.IndexNotAvailableException; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.NotCMSException; import org.apache.cassandra.utils.NoSpamLogger; /** @@ -72,10 +78,29 @@ public class InboundSink implements InboundMessageHandlers.MessageConsumer private final MessagingService messaging; + private final static EnumSet allowedDuringStartup = EnumSet.of(Verb.GOSSIP_DIGEST_ACK, Verb.GOSSIP_DIGEST_SYN); + InboundSink(MessagingService messaging) { this.messaging = messaging; - this.sink = message -> message.header.verb.handler().doVerb((Message) message); + this.sink = message -> { + IVerbHandler handler = message.header.verb.handler(); + if (handler == null) + { + String err = String.format("Handler for verb %s is null", message.header.verb); + noSpamLogger.info(err); + throw new IllegalStateException(err); + } + + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata != null && metadata.epoch.is(Epoch.UPGRADE_STARTUP) && !allowedDuringStartup.contains(message.header.verb)) + { + noSpamLogger.info("Ignoring message from {} with verb="+message.header.verb, message.from()); + return; + } + + handler.doVerb(message); + }; } public void fail(Message.Header header, Throwable failure) @@ -100,7 +125,9 @@ public class InboundSink implements InboundMessageHandlers.MessageConsumer { fail(message.header, t); - if (t instanceof TombstoneOverwhelmingException || t instanceof IndexNotAvailableException) + if (t instanceof NotCMSException || t instanceof CoordinatorBehindException) + noSpamLogger.warn(t.getMessage()); + else if (t instanceof TombstoneOverwhelmingException || t instanceof IndexNotAvailableException || t instanceof InvalidRoutingException) noSpamLogger.error(t.getMessage()); else if (t instanceof RuntimeException) throw (RuntimeException) t; diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java index c60ebee972..a20125c5a6 100644 --- a/src/java/org/apache/cassandra/net/Message.java +++ b/src/java/org/apache/cassandra/net/Message.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.Ints; @@ -40,11 +41,11 @@ import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.tracing.Tracing.TraceType; -import org.apache.cassandra.utils.MonotonicClockTranslation; -import org.apache.cassandra.utils.NoSpamLogger; -import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.*; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -67,13 +68,18 @@ public class Message private static final Logger logger = LoggerFactory.getLogger(Message.class); private static final NoSpamLogger noSpam1m = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES); + private static final Supplier epochSupplier = () -> ClusterMetadata.current().epoch; + public final Header header; public final T payload; + private final IVersionedAsymmetricSerializer payloadSerializer; + Message(Header header, T payload) { this.header = header; this.payload = payload; + this.payloadSerializer = verb().serializer(); } /** Sender of the message. */ @@ -97,6 +103,11 @@ public class Message return header.id; } + public Epoch epoch() + { + return header.epoch; + } + public Verb verb() { return header.verb; @@ -190,14 +201,14 @@ public class Message */ public static Message out(Verb verb, T payload) { - assert !verb.isResponse(); + assert !verb.isResponse() : verb; return outWithParam(nextId(), verb, payload, null, null); } public static Message synthetic(InetAddressAndPort from, Verb verb, T payload) { - return new Message<>(new Header(-1, verb, from, -1, -1, 0, NO_PARAMS), payload); + return new Message<>(new Header(-1, epochSupplier.get(), verb, from, -1, -1, 0, NO_PARAMS), payload); } public static Message out(Verb verb, T payload, long expiresAtNanos) @@ -242,7 +253,7 @@ public class Message if (expiresAtNanos == 0) expiresAtNanos = verb.expiresAtNanos(createdAtNanos); - return new Message<>(new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, buildParams(paramType, paramValue)), payload); + return new Message<>(new Header(id, epochSupplier.get(), verb, from, createdAtNanos, expiresAtNanos, flags, buildParams(paramType, paramValue)), payload); } public static Message internalResponse(Verb verb, T payload) @@ -260,7 +271,34 @@ public class Message assert verb.isResponse(); long createdAtNanos = approxTime.now(); long expiresAtNanos = verb.expiresAtNanos(createdAtNanos); - return new Message<>(new Header(0, verb, from, createdAtNanos, expiresAtNanos, 0, NO_PARAMS), payload); + return new Message<>(new Header(0, epochSupplier.get(), verb, from, createdAtNanos, expiresAtNanos, 0, NO_PARAMS), payload); + } + + /** + * A way to generate messages originating from arbitrary node. Should ONLY be used for testing purposes, as meddling with an + * originator can be dangerous. + */ + @VisibleForTesting + public static Message remoteResponseForTests(long id, InetAddressAndPort from, Verb verb, T payload) + { + assert verb.isResponse(); + long createdAtNanos = approxTime.now(); + long expiresAtNanos = verb.expiresAtNanos(createdAtNanos); + return new Message<>(new Header(id, epochSupplier.get(), verb, from, createdAtNanos, expiresAtNanos, 0, NO_PARAMS), payload); + } + + @VisibleForTesting + public static Message forgeIdentityForTests(Message msg, InetAddressAndPort addr) + { + return new Message<>(new Header(msg.header.id, + msg.header.epoch, + msg.header.verb, + addr, + msg.header.createdAtNanos, + msg.header.expiresAtNanos, + msg.header.flags, + msg.header.params), + msg.payload); } /** Builds a response Message with provided payload, and all the right fields inferred from request Message */ @@ -311,6 +349,11 @@ public class Message return new Message<>(header.withFlag(flag), payload); } + public Message withEpoch(Epoch epoch) + { + return new Message<>(header.withEpoch(epoch), payload); + } + public Message withParam(ParamType type, Object value) { return new Message<>(header.withParam(type, value), payload); @@ -421,6 +464,7 @@ public class Message public static class Header { public final long id; + public final Epoch epoch; public final Verb verb; public final InetAddressAndPort from; public final long createdAtNanos; @@ -428,9 +472,10 @@ public class Message private final int flags; private final Map params; - private Header(long id, Verb verb, InetAddressAndPort from, long createdAtNanos, long expiresAtNanos, int flags, Map params) + private Header(long id, Epoch epoch, Verb verb, InetAddressAndPort from, long createdAtNanos, long expiresAtNanos, int flags, Map params) { this.id = id; + this.epoch = epoch; this.verb = verb; this.from = from; this.expiresAtNanos = expiresAtNanos; @@ -441,22 +486,27 @@ public class Message Header withFrom(InetAddressAndPort from) { - return new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, params); + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flags, params); } Header withFlag(MessageFlag flag) { - return new Header(id, verb, from, createdAtNanos, expiresAtNanos, flag.addTo(flags), params); + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flag.addTo(flags), params); + } + + Header withEpoch(Epoch epoch) + { + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flags, params); } Header withParam(ParamType type, Object value) { - return new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, addParam(params, type, value)); + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flags, addParam(params, type, value)); } Header withParams(Map values) { - return new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, addParams(params, values)); + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flags, addParams(params, values)); } boolean callBackOnFailure() @@ -523,6 +573,7 @@ public class Message private long createdAtNanos; private long expiresAtNanos; private long id; + private Epoch epoch; private boolean hasId; @@ -624,6 +675,12 @@ public class Message return this; } + public Builder withEpoch(Epoch epoch) + { + this.epoch = epoch; + return this; + } + public Message build() { if (verb == null) @@ -632,8 +689,10 @@ public class Message throw new IllegalArgumentException(); if (payload == null) throw new IllegalArgumentException(); + if (epoch == null) + epoch = epochSupplier.get(); - return new Message<>(new Header(hasId ? id : nextId(), verb, from, createdAtNanos, expiresAtNanos, flags, params), payload); + return new Message<>(new Header(hasId ? id : nextId(), epoch, verb, from, createdAtNanos, expiresAtNanos, flags, params), payload); } } @@ -646,6 +705,7 @@ public class Message .withExpiresAt(message.expiresAtNanos()) .withFlags(message.header.flags) .withParams(message.header.params) + .withEpoch(message.header.epoch) .withPayload(message.payload); } @@ -684,6 +744,8 @@ public class Message * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | Message ID (vint) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Epoch (vint) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | Creation timestamp (int) | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | Expiry (vint) | @@ -737,7 +799,7 @@ public class Message */ public Message deserialize(DataInputPlus in, Header header, int version) throws IOException { - skipHeader(in); + skipHeader(in, version); skipUnsignedVInt(in); // payload size, not needed by payload deserializer T payload = (T) header.verb.serializer().deserialize(in, version); return new Message<>(header, payload); @@ -755,7 +817,7 @@ public class Message /** * Size of the next message in the stream. Returns -1 if there aren't sufficient bytes read yet to determine size. */ - int inferMessageSize(ByteBuffer buf, int readerIndex, int readerLimit) + int inferMessageSize(ByteBuffer buf, int readerIndex, int readerLimit, int version) { int index = readerIndex; @@ -764,6 +826,14 @@ public class Message return -1; // not enough bytes to read id index += idSize; + if (version >= VERSION_50) + { + int epochSize = computeUnsignedVIntSize(buf, index, readerLimit); + if (epochSize < 0) + return -1; // not enough bytes to read epoch + index += epochSize; + } + index += CREATION_TIME_SIZE; if (index > readerLimit) return -1; @@ -817,6 +887,13 @@ public class Message long id = getUnsignedVInt(buf, index); index += computeUnsignedVIntSize(id); + Epoch epoch = Epoch.EMPTY; + if (version >= VERSION_50) + { + long epochl = getUnsignedVInt(buf, index); + index += computeUnsignedVIntSize(epochl); + epoch = Epoch.create(epochl); + } int createdAtMillis = buf.getInt(index); index += sizeof(createdAtMillis); @@ -834,7 +911,7 @@ public class Message long createdAtNanos = calculateCreationTimeNanos(createdAtMillis, timeSnapshot, currentTimeNanos); long expiresAtNanos = getExpiresAtNanos(createdAtNanos, currentTimeNanos, TimeUnit.MILLISECONDS.toNanos(expiresInMillis)); - return new Header(id, verb, from, createdAtNanos, expiresAtNanos, flags, params); + return new Header(id, epoch, verb, from, createdAtNanos, expiresAtNanos, flags, params); } private static long getExpiresAtNanos(long createdAtNanos, long currentTimeNanos, long expirationPeriodNanos) @@ -847,6 +924,8 @@ public class Message private void serializeHeader(Header header, DataOutputPlus out, int version) throws IOException { out.writeUnsignedVInt(header.id); + if (version >= VERSION_50) + Epoch.messageSerializer.serialize(header.epoch, out, version); // int cast cuts off the high-order half of the timestamp, which we can assume remains // the same between now and when the recipient reconstructs it. out.writeInt((int) approxTime.translate().toMillisSinceEpoch(header.createdAtNanos)); @@ -859,6 +938,9 @@ public class Message private Header deserializeHeader(DataInputPlus in, InetAddressAndPort peer, int version) throws IOException { long id = in.readUnsignedVInt(); + Epoch epoch = Epoch.EMPTY; + if (version >= VERSION_50) + epoch = Epoch.messageSerializer.deserialize(in, version); long currentTimeNanos = approxTime.now(); MonotonicClockTranslation timeSnapshot = approxTime.translate(); long creationTimeNanos = calculateCreationTimeNanos(in.readInt(), timeSnapshot, currentTimeNanos); @@ -866,12 +948,15 @@ public class Message Verb verb = Verb.fromId(in.readUnsignedVInt32()); int flags = in.readUnsignedVInt32(); Map params = deserializeParams(in, version); - return new Header(id, verb, peer, creationTimeNanos, expiresAtNanos, flags, params); + + return new Header(id, epoch, verb, peer, creationTimeNanos, expiresAtNanos, flags, params); } - private void skipHeader(DataInputPlus in) throws IOException + private void skipHeader(DataInputPlus in, int version) throws IOException { skipUnsignedVInt(in); // id + if (version >= VERSION_50) + skipUnsignedVInt(in); // epoch in.skipBytesFully(4); // createdAt skipUnsignedVInt(in); // expiresIn skipUnsignedVInt(in); // verb @@ -883,6 +968,8 @@ public class Message { long size = 0; size += sizeofUnsignedVInt(header.id); + if (version >= VERSION_50) + size += sizeofUnsignedVInt(header.epoch.getEpoch()); size += CREATION_TIME_SIZE; size += sizeofUnsignedVInt(NANOSECONDS.toMillis(header.expiresAtNanos - header.createdAtNanos)); size += sizeofUnsignedVInt(header.verb.id); @@ -1072,7 +1159,7 @@ public class Message private IVersionedAsymmetricSerializer getPayloadSerializer() { - return verb().serializer(); + return payloadSerializer; } private int serializedSize40; @@ -1094,7 +1181,7 @@ public class Message serializedSize50 = serializer.serializedSize(this, VERSION_50); return serializedSize50; default: - throw new IllegalStateException("Unkown serialization version " + version); + throw new IllegalStateException("Unknown serialization version " + version); } } diff --git a/src/java/org/apache/cassandra/net/MessageDelivery.java b/src/java/org/apache/cassandra/net/MessageDelivery.java index dd8c6ceeda..cd2ea96d1f 100644 --- a/src/java/org/apache/cassandra/net/MessageDelivery.java +++ b/src/java/org/apache/cassandra/net/MessageDelivery.java @@ -18,11 +18,55 @@ package org.apache.cassandra.net; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.concurrent.Accumulator; +import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.Future; public interface MessageDelivery { + Logger logger = LoggerFactory.getLogger(MessageDelivery.class); + + static Collection> fanoutAndWait(MessageDelivery messaging, Set sendTo, Verb verb, REQ payload) + { + Accumulator> responses = new Accumulator<>(sendTo.size()); + CountDownLatch cdl = CountDownLatch.newCountDownLatch(sendTo.size()); + RequestCallback callback = new RequestCallbackWithFailure<>() + { + @Override + public void onResponse(Message msg) + { + logger.info("Received a {} response from {}: {}", msg.verb(), msg.from(), msg.payload); + responses.add(Pair.create(msg.from(), msg.payload)); + cdl.decrement(); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason reason) + { + logger.info("Received failure in response to {} from {}: {}", verb, from, reason); + cdl.decrement(); + } + }; + + sendTo.forEach((ep) -> { + logger.info("Election for metadata migration sending {} ({}) to {}", verb, payload.toString(), ep); + messaging.sendWithCallback(Message.out(verb, payload), ep, callback); + }); + cdl.awaitUninterruptibly(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + return responses.snapshot(); + } + public void send(Message message, InetAddressAndPort to); public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb); public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection); diff --git a/src/java/org/apache/cassandra/net/MessagingService.java b/src/java/org/apache/cassandra/net/MessagingService.java index d3c3d96565..f4eaf72571 100644 --- a/src/java/org/apache/cassandra/net/MessagingService.java +++ b/src/java/org/apache/cassandra/net/MessagingService.java @@ -48,6 +48,7 @@ import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.FutureCombiner; +import org.apache.cassandra.utils.concurrent.Promise; import static java.util.Collections.synchronizedList; import static java.util.concurrent.TimeUnit.MINUTES; @@ -380,12 +381,14 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa * @param cb callback interface which is used to pass the responses or * suggest that a timeout occurred to the invoker of the send(). */ - public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) + @Override + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) { sendWithCallback(message, to, cb, null); } - public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) + @Override + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) { callbacks.addWithExpiration(cb, message, to); if (cb.invokeOnFailure() && !message.callBackOnFailure()) @@ -418,7 +421,8 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa * @param message messages to be sent. * @param to endpoint to which the message needs to be sent */ - public void send(Message message, InetAddressAndPort to) + @Override + public void send(Message message, InetAddressAndPort to) { send(message, to, null); } @@ -435,6 +439,28 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa send(message.responseWith(response), message.respondTo()); } + public Future sendWithResponse(InetAddressAndPort to, Message msg) + { + Promise future = AsyncPromise.uncancellable(); + MessagingService.instance().sendWithCallback(msg, to, + new RequestCallback() + { + @Override + public void onResponse(Message msg) + { + future.setSuccess(msg.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + future.setFailure(new RuntimeException(failureReason.toString())); + } + }); + + return future; + } + public void respondWithFailure(RequestFailureReason reason, Message message) { send(Message.failureResponse(message.id(), message.expiresAtNanos(), reason), message.respondTo()); @@ -442,6 +468,12 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa public void send(Message message, InetAddressAndPort to, ConnectionType specifyConnection) { + if (isShuttingDown) + { + logger.error("Cannot send the message {} to {}, as messaging service is shutting down", message, to); + return; + } + if (logger.isTraceEnabled()) { logger.trace("{} sending {} to {}@{}", FBUtilities.getBroadcastAddressAndPort(), message.verb(), message.id(), to); @@ -691,4 +723,16 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa { inboundSockets.open().await(); } + + public void waitUntilListeningUnchecked() + { + try + { + inboundSockets.open().await(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } } diff --git a/src/java/org/apache/cassandra/net/OutboundConnection.java b/src/java/org/apache/cassandra/net/OutboundConnection.java index cfb9f1ffc0..4aa754d8aa 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnection.java +++ b/src/java/org/apache/cassandra/net/OutboundConnection.java @@ -34,6 +34,7 @@ import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.slf4j.Logger; @@ -61,6 +62,7 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.lang.Math.max; import static java.lang.Math.min; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.apache.cassandra.net.InternodeConnectionUtils.isSSLError; import static org.apache.cassandra.net.MessagingService.current_version; import static org.apache.cassandra.net.OutboundConnectionInitiator.*; @@ -474,7 +476,13 @@ public class OutboundConnection */ private boolean onExpired(Message message) { - noSpamLogger.warn("{} dropping message of type {} whose timeout expired before reaching the network", id(), message.verb()); + if (logger.isTraceEnabled()) + logger.trace("{} dropping message of type {} with payload {} whose timeout ({}ms) expired before reaching the network. {}ms elapsed after expiration. {}ms since creation.", + id(), message.verb(), message.payload, DatabaseDescriptor.getRpcTimeout(MILLISECONDS), + NANOSECONDS.toMillis(Clock.Global.nanoTime() - message.expiresAtNanos()), + message.elapsedSinceCreated(MILLISECONDS)); + else + noSpamLogger.warn("{} dropping message of type {} whose timeout expired before reaching the network", id(), message.verb()); releaseCapacity(1, canonicalSize(message)); expiredCount += 1; expiredBytes += canonicalSize(message); diff --git a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java index 1cee468cd3..a4742840f6 100644 --- a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java +++ b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java @@ -17,13 +17,22 @@ */ package org.apache.cassandra.net; +import java.util.EnumSet; +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.FBUtilities; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.exceptions.RequestFailureReason.COORDINATOR_BEHIND; +import static org.apache.cassandra.exceptions.RequestFailureReason.INVALID_ROUTING; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; class ResponseVerbHandler implements IVerbHandler @@ -31,7 +40,21 @@ class ResponseVerbHandler implements IVerbHandler public static final ResponseVerbHandler instance = new ResponseVerbHandler(); private static final Logger logger = LoggerFactory.getLogger(ResponseVerbHandler.class); + private static final Set SKIP_CATCHUP_FOR = EnumSet.of(Verb.TCM_FETCH_CMS_LOG_RSP, + Verb.TCM_FETCH_PEER_LOG_RSP, + Verb.TCM_COMMIT_RSP, + Verb.TCM_REPLICATION, + Verb.TCM_NOTIFY_RSP, + Verb.TCM_DISCOVER_RSP, + Verb.TCM_INIT_MIG_RSP); + // We skip epoch catchup for PaxosV2 verbs, since we are using PaxosV2 to serially read the log. + private static final Set CMS_SKIP_CATCHUP_FOR = EnumSet.of(Verb.PAXOS2_COMMIT_REMOTE_REQ, Verb.PAXOS2_COMMIT_REMOTE_RSP, Verb.PAXOS2_PREPARE_RSP, Verb.PAXOS2_PREPARE_REQ, + Verb.PAXOS2_PREPARE_REFRESH_RSP, Verb.PAXOS2_PREPARE_REFRESH_REQ, Verb.PAXOS2_PROPOSE_RSP, Verb.PAXOS2_PROPOSE_REQ, + Verb.PAXOS2_COMMIT_AND_PREPARE_RSP, Verb.PAXOS2_COMMIT_AND_PREPARE_REQ, Verb.PAXOS2_REPAIR_RSP, Verb.PAXOS2_REPAIR_REQ, + Verb.PAXOS2_CLEANUP_START_PREPARE_RSP, Verb.PAXOS2_CLEANUP_START_PREPARE_REQ, Verb.PAXOS2_CLEANUP_RSP, Verb.PAXOS2_CLEANUP_REQ, + Verb.PAXOS2_CLEANUP_RSP2, Verb.PAXOS2_CLEANUP_FINISH_PREPARE_RSP, Verb.PAXOS2_CLEANUP_FINISH_PREPARE_REQ, + Verb.PAXOS2_CLEANUP_COMPLETE_RSP, Verb.PAXOS2_CLEANUP_COMPLETE_REQ); @Override public void doVerb(Message message) { @@ -46,7 +69,7 @@ class ResponseVerbHandler implements IVerbHandler long latencyNanos = approxTime.now() - callbackInfo.createdAtNanos; Tracing.trace("Processing response from {}", message.from()); - + maybeFetchLogs(message); RequestCallback cb = callbackInfo.callback; if (message.isFailureResponse()) { @@ -58,4 +81,44 @@ class ResponseVerbHandler implements IVerbHandler cb.onResponse(message); } } + + private void maybeFetchLogs(Message message) + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (!message.epoch().isAfter(metadata.epoch)) + return; + + if (SKIP_CATCHUP_FOR.contains(message.verb())) + return; + + if (metadata.isCMSMember(FBUtilities.getBroadcastAddressAndPort()) && CMS_SKIP_CATCHUP_FOR.contains(message.verb())) + return; + + // Gossip stage is single-threaded, so we may end up in a deadlock with after-commit hook + // that executes something on the gossip stage as well. + if (message.isFailureResponse() && + (message.payload == COORDINATOR_BEHIND || message.payload == INVALID_ROUTING) && + // Gossip stage is single-threaded, so we may end up in a deadlock with after-commit hook + // that executes something on the gossip stage as well. + !Stage.GOSSIP.executor().inExecutor()) + { + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + + if (metadata.epoch.isEqualOrAfter(message.epoch())) + logger.debug("Learned about next epoch {} from {} in {}", message.epoch(), message.from(), message.verb()); + } + else + { + ClusterMetadataService.instance().fetchLogFromPeerAsync(message.from(), message.epoch()); + return; + } + + // We have to perform this operation in a blocking way, since otherwise we can violate consistency. For example, by + // missing a write to pending replica. + // TODO: check if we can relax it again, via COORDINATOR_BEHIND + metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch()); + + if (metadata.epoch.isEqualOrAfter(message.epoch())) + logger.debug("Learned about next epoch {} from {} in {}", message.epoch(), message.from(), message.verb()); + } } diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java index c85f0ddeca..885dfdae25 100644 --- a/src/java/org/apache/cassandra/net/Verb.java +++ b/src/java/org/apache/cassandra/net/Verb.java @@ -88,6 +88,15 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupResponse; import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupComplete; import org.apache.cassandra.service.paxos.cleanup.PaxosStartPrepareCleanup; import org.apache.cassandra.service.paxos.cleanup.PaxosFinishPrepareCleanup; +import org.apache.cassandra.streaming.DataMovement; +import org.apache.cassandra.streaming.DataMovementVerbHandler; +import org.apache.cassandra.tcm.Discovery; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.FetchCMSLog; +import org.apache.cassandra.tcm.FetchPeerLog; +import org.apache.cassandra.tcm.migration.Election; +import org.apache.cassandra.tcm.sequences.DataMovements; +import org.apache.cassandra.tcm.serialization.MessageSerializers; import org.apache.cassandra.utils.BooleanSerializer; import org.apache.cassandra.service.EchoVerbHandler; import org.apache.cassandra.service.SnapshotVerbHandler; @@ -106,6 +115,11 @@ import static org.apache.cassandra.concurrent.Stage.*; import static org.apache.cassandra.net.VerbTimeouts.*; import static org.apache.cassandra.net.Verb.Kind.*; import static org.apache.cassandra.net.Verb.Priority.*; +import static org.apache.cassandra.tcm.ClusterMetadataService.commitRequestHandler; +import static org.apache.cassandra.tcm.ClusterMetadataService.currentEpochRequestHandler; +import static org.apache.cassandra.tcm.ClusterMetadataService.logNotifyHandler; +import static org.apache.cassandra.tcm.ClusterMetadataService.fetchLogRequestHandler; +import static org.apache.cassandra.tcm.ClusterMetadataService.replicationHandler; /** * Note that priorities except P0 are presently unused. P0 corresponds to urgent, i.e. what used to be the "Gossip" connection. @@ -152,9 +166,13 @@ public enum Verb PING_REQ (37, P1, pingTimeout, GOSSIP, () -> PingRequest.serializer, () -> PingVerbHandler.instance, PING_RSP ), // P1 because messages can be arbitrarily large or aren't crucial + @Deprecated (since = "CEP-21") SCHEMA_PUSH_RSP (98, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), + @Deprecated (since = "CEP-21") SCHEMA_PUSH_REQ (18, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> SchemaPushVerbHandler.instance, SCHEMA_PUSH_RSP ), + @Deprecated (since = "CEP-21") SCHEMA_PULL_RSP (88, P1, rpcTimeout, MIGRATION, () -> SchemaMutationsSerializer.instance, () -> ResponseVerbHandler.instance ), + @Deprecated (since = "CEP-21") SCHEMA_PULL_REQ (28, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaPullVerbHandler.instance, SCHEMA_PULL_RSP ), SCHEMA_VERSION_RSP (80, P1, rpcTimeout, MIGRATION, () -> UUIDSerializer.serializer, () -> ResponseVerbHandler.instance ), SCHEMA_VERSION_REQ (20, P1, rpcTimeout, MIGRATION, () -> NoPayload.serializer, () -> SchemaVersionVerbHandler.instance, SCHEMA_VERSION_RSP ), @@ -205,6 +223,28 @@ public enum Verb PAXOS2_CLEANUP_COMPLETE_RSP (59, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), PAXOS2_CLEANUP_COMPLETE_REQ (48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP ), + // transactional cluster metadata + TCM_COMMIT_RSP (801, P1, rpcTimeout, INTERNAL_METADATA, MessageSerializers::commitResultSerializer, () -> ResponseVerbHandler.instance ), + TCM_COMMIT_REQ (802, P1, rpcTimeout, INTERNAL_METADATA, MessageSerializers::commitSerializer, () -> commitRequestHandler(), TCM_COMMIT_RSP ), + TCM_FETCH_CMS_LOG_RSP (803, P1, rpcTimeout, FETCH_LOG, MessageSerializers::logStateSerializer, () -> ResponseVerbHandler.instance ), + TCM_FETCH_CMS_LOG_REQ (804, P1, rpcTimeout, FETCH_LOG, () -> FetchCMSLog.serializer, () -> fetchLogRequestHandler(), TCM_FETCH_CMS_LOG_RSP ), + TCM_REPLICATION (805, P1, rpcTimeout, INTERNAL_METADATA, MessageSerializers::replicationSerializer, () -> replicationHandler() ), + TCM_NOTIFY_RSP (806, P1, rpcTimeout, INTERNAL_METADATA, () -> Epoch.messageSerializer, () -> ResponseVerbHandler.instance ), + TCM_NOTIFY_REQ (807, P1, rpcTimeout, INTERNAL_METADATA, MessageSerializers::logStateSerializer, () -> logNotifyHandler(), TCM_NOTIFY_RSP ), + TCM_CURRENT_EPOCH_REQ (808, P1, rpcTimeout, INTERNAL_METADATA, () -> Epoch.messageSerializer, () -> currentEpochRequestHandler(), TCM_NOTIFY_RSP ), + TCM_INIT_MIG_RSP (809, P1, rpcTimeout, INTERNAL_METADATA, MessageSerializers::metadataHolderSerializer, () -> ResponseVerbHandler.instance ), + TCM_INIT_MIG_REQ (810, P1, rpcTimeout, INTERNAL_METADATA, () -> Election.Initiator.serializer, () -> Election.instance.prepareHandler, TCM_INIT_MIG_RSP ), + TCM_ABORT_MIG (811, P1, rpcTimeout, INTERNAL_METADATA, () -> Election.Initiator.serializer, () -> Election.instance.abortHandler, TCM_INIT_MIG_RSP ), + TCM_DISCOVER_RSP (812, P1, rpcTimeout, INTERNAL_METADATA, () -> Discovery.serializer, () -> ResponseVerbHandler.instance ), + TCM_DISCOVER_REQ (813, P1, rpcTimeout, INTERNAL_METADATA, () -> NoPayload.serializer, () -> Discovery.instance.requestHandler, TCM_DISCOVER_RSP ), + TCM_FETCH_PEER_LOG_RSP (818, P1, rpcTimeout, FETCH_LOG, MessageSerializers::logStateSerializer, () -> ResponseVerbHandler.instance ), + TCM_FETCH_PEER_LOG_REQ (819, P1, rpcTimeout, FETCH_LOG, () -> FetchPeerLog.serializer, () -> FetchPeerLog.Handler.instance, TCM_FETCH_PEER_LOG_RSP ), + + INITIATE_DATA_MOVEMENTS_RSP (814, P1, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), + INITIATE_DATA_MOVEMENTS_REQ (815, P1, rpcTimeout, MISC, () -> DataMovement.serializer, () -> DataMovementVerbHandler.instance, INITIATE_DATA_MOVEMENTS_RSP ), + DATA_MOVEMENT_EXECUTED_RSP (816, P1, rpcTimeout, MISC, () -> NoPayload.serializer, () -> ResponseVerbHandler.instance ), + DATA_MOVEMENT_EXECUTED_REQ (817, P1, rpcTimeout, MISC, () -> DataMovement.Status.serializer, () -> DataMovements.instance, DATA_MOVEMENT_EXECUTED_RSP ), + // generic failure response FAILURE_RSP (99, P0, noTimeout, REQUEST_RESPONSE, () -> RequestFailureReason.serializer, () -> ResponseVerbHandler.instance ), diff --git a/src/java/org/apache/cassandra/repair/RepairCoordinator.java b/src/java/org/apache/cassandra/repair/RepairCoordinator.java index 3d1eab3e9e..4fcd788e36 100644 --- a/src/java/org/apache/cassandra/repair/RepairCoordinator.java +++ b/src/java/org/apache/cassandra/repair/RepairCoordinator.java @@ -362,8 +362,8 @@ public class RepairCoordinator implements Runnable, ProgressEventNotifier, Repai for (Range range : state.options.getRanges()) { EndpointsForRange neighbors = ctx.repair().getNeighbors(state.keyspace, keyspaceLocalRanges, range, - state.options.getDataCenters(), - state.options.getHosts()); + state.options.getDataCenters(), + state.options.getHosts()); if (neighbors.isEmpty()) { if (state.options.ignoreUnreplicatedKeyspaces()) diff --git a/src/java/org/apache/cassandra/repair/RepairJob.java b/src/java/org/apache/cassandra/repair/RepairJob.java index 8fec3e9d54..e88ef970af 100644 --- a/src/java/org/apache/cassandra/repair/RepairJob.java +++ b/src/java/org/apache/cassandra/repair/RepairJob.java @@ -61,6 +61,7 @@ import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.ImmediateFuture; import static org.apache.cassandra.config.DatabaseDescriptor.paxosRepairEnabled; +import static org.apache.cassandra.schema.SchemaConstants.METADATA_KEYSPACE_NAME; import static org.apache.cassandra.service.paxos.Paxos.useV2; /** @@ -128,7 +129,7 @@ public class RepairJob extends AsyncFuture implements Runnable Future> treeResponses; Future paxosRepair; - if (paxosRepairEnabled() && ((useV2() && session.repairPaxos) || session.paxosOnly)) + if (paxosRepairEnabled() && (((useV2() || isMetadataKeyspace()) && session.repairPaxos) || session.paxosOnly)) { logger.info("{} {}.{} starting paxos repair", session.previewKind.logPrefix(session.getId()), desc.keyspace, desc.columnFamily); TableMetadata metadata = Schema.instance.getTableMetadata(desc.keyspace, desc.columnFamily); @@ -260,6 +261,11 @@ public class RepairJob extends AsyncFuture implements Runnable s.abort(reason); } + private boolean isMetadataKeyspace() + { + return desc.keyspace.equals(METADATA_KEYSPACE_NAME); + } + private boolean isTransient(InetAddressAndPort ep) { return session.state.commonRange.transEndpoints.contains(ep); diff --git a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java index 34f20cd708..d59ff7e0c9 100644 --- a/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java +++ b/src/java/org/apache/cassandra/repair/RepairMessageVerbHandler.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.repair.messages.*; @@ -36,6 +37,7 @@ import org.apache.cassandra.repair.state.SyncState; import org.apache.cassandra.repair.state.ValidationState; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.TimeUUID; @@ -226,6 +228,17 @@ public class RepairMessageVerbHandler implements IVerbHandler sendFailureResponse(message); return; } + + if (!acceptMessage(validationRequest, ctx.broadcastAddressAndPort(), message.from())) + { + RepairOutOfTokenRangeException e = new RepairOutOfTokenRangeException(validationRequest.desc.ranges); + + logger.error("Got out-of-range repair request from " + message.from() + ": " + validationRequest.desc.ranges, e); + vState.phase.fail(e); + sendFailureResponse(message); + return; + } + vState.phase.accept(); sendAck(message); @@ -417,4 +430,14 @@ public class RepairMessageVerbHandler implements IVerbHandler { RepairMessage.sendAck(ctx, message); } + + private static boolean acceptMessage(final ValidationRequest validationRequest, InetAddressAndPort broadcastAddressAndPort, final InetAddressAndPort from) + { + return StorageService.instance + .getNormalizedLocalRanges(validationRequest.desc.keyspace, broadcastAddressAndPort) + .validateRangeRequest(validationRequest.desc.ranges, + "RepairSession #" + validationRequest.desc.parentSessionId, + "validation request", + from); + } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceMarkShutdown.java b/src/java/org/apache/cassandra/repair/RepairOutOfTokenRangeException.java similarity index 68% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceMarkShutdown.java rename to src/java/org/apache/cassandra/repair/RepairOutOfTokenRangeException.java index 6ef615faaa..0b5f24a9fe 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceMarkShutdown.java +++ b/src/java/org/apache/cassandra/repair/RepairOutOfTokenRangeException.java @@ -16,14 +16,17 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.repair; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.markShutdownRunner; +import java.util.Collection; -class OnInstanceMarkShutdown extends ClusterReliableAction +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +public class RepairOutOfTokenRangeException extends RuntimeException { - OnInstanceMarkShutdown(ClusterActions actions, int on) + public RepairOutOfTokenRangeException(Collection> ownedRanges) { - super("Mark Self Shutdown on " + on, actions, on, markShutdownRunner(actions.cluster.get(on).broadcastAddress())); + super(String.format("Received repair outside of owned ranges %s", ownedRanges)); } } diff --git a/src/java/org/apache/cassandra/repair/RepairSession.java b/src/java/org/apache/cassandra/repair/RepairSession.java index e0e104ff8e..9dd97acb2d 100644 --- a/src/java/org/apache/cassandra/repair/RepairSession.java +++ b/src/java/org/apache/cassandra/repair/RepairSession.java @@ -270,7 +270,7 @@ public class RepairSession extends AsyncFuture implements I private String repairedNodes() { StringBuilder sb = new StringBuilder(); - sb.append(FBUtilities.getBroadcastAddressAndPort()); + sb.append(ctx.broadcastAddressAndPort()); for (InetAddressAndPort ep : state.commonRange.endpoints) sb.append(", ").append(ep); return sb.toString(); diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java index f68a7b5ff3..7350c40e7f 100644 --- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java +++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; import java.util.function.Predicate; @@ -35,12 +36,21 @@ import org.apache.cassandra.cql3.selection.SimpleSelector; import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; import org.github.jamm.Unmetered; +import static org.apache.cassandra.db.TypeSizes.BOOL_SIZE; +import static org.apache.cassandra.db.TypeSizes.sizeof; + @Unmetered public final class ColumnMetadata extends ColumnSpecification implements Selectable, Comparable { + public static final Serializer serializer = new Serializer(); public static final Comparator asymmetricColumnDataComparator = (a, b) -> ((ColumnData) a).column().compareTo((ColumnMetadata) b); @@ -539,4 +549,55 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta { return type; } + + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(ColumnMetadata t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.ksName); + out.writeUTF(t.cfName); + out.writeUTF(t.kind.name()); + out.writeInt(t.position); + out.writeUTF(t.type.asCQL3Type().toString()); + out.writeBoolean(t.isReversedType()); + out.writeUTF(t.name.toString()); + ByteBufferUtil.writeWithShortLength(t.name.bytes, out); + out.writeBoolean(t.mask != null); + if (t.mask != null) + ColumnMask.serializer.serialize(t.mask, out, version); + } + + public ColumnMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + String ksName = in.readUTF(); + String tableName = in.readUTF(); + Kind kind = Kind.valueOf(in.readUTF()); + int position = in.readInt(); + AbstractType type = CQLTypeParser.parse(ksName, in.readUTF(), types); + boolean isReversedType = in.readBoolean(); + if (isReversedType) + type = ReversedType.getInstance(type); + String name = in.readUTF(); + ByteBuffer nameBB = ByteBufferUtil.readWithShortLength(in); + ColumnMask mask = null; + boolean masked = in.readBoolean(); + if (masked) + mask = ColumnMask.serializer.deserialize(in, ksName, type, types, functions, version); + return new ColumnMetadata(ksName, tableName, new ColumnIdentifier(nameBB, name), type, position, kind, mask); + } + + public long serializedSize(ColumnMetadata t, Version version) + { + return sizeof(t.ksName) + + sizeof(t.cfName) + + sizeof(t.kind.name()) + + sizeof(t.position) + + sizeof(t.type.asCQL3Type().toString()) + + sizeof(t.isReversedType()) + + sizeof(t.name.toString()) + + ByteBufferUtil.serializedSizeWithShortLength(t.name.bytes) + + BOOL_SIZE + + ((t.mask == null) ? 0 : ColumnMask.serializer.serializedSize(t.mask, version)); + } + } } diff --git a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java b/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java deleted file mode 100644 index 0bea57712d..0000000000 --- a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandler.java +++ /dev/null @@ -1,356 +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.schema; - -import java.time.Duration; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; - -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ScheduledExecutors; -import org.apache.cassandra.concurrent.Stage; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Mutation; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.IEndpointStateChangeSubscriber; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.concurrent.AsyncPromise; -import org.apache.cassandra.utils.concurrent.Awaitable; - -import static org.apache.cassandra.schema.MigrationCoordinator.MAX_OUTSTANDING_VERSION_REQUESTS; - -public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpointStateChangeSubscriber -{ - private static final Logger logger = LoggerFactory.getLogger(DefaultSchemaUpdateHandler.class); - - @VisibleForTesting - final MigrationCoordinator migrationCoordinator; - - private final boolean requireSchemas; - private final BiConsumer updateCallback; - private volatile DistributedSchema schema = DistributedSchema.EMPTY; - - private volatile AsyncPromise requestedReset; - - private MigrationCoordinator createMigrationCoordinator(MessagingService messagingService) - { - return new MigrationCoordinator(messagingService, - Stage.MIGRATION.executor(), - ScheduledExecutors.scheduledTasks, - MAX_OUTSTANDING_VERSION_REQUESTS, - Gossiper.instance, - this::getSchemaVersionForCoordinator, - this::applyMutationsFromCoordinator); - } - - public DefaultSchemaUpdateHandler(BiConsumer updateCallback) - { - this(null, MessagingService.instance(), !CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK.getBoolean(), updateCallback); - } - - public DefaultSchemaUpdateHandler(MigrationCoordinator migrationCoordinator, - MessagingService messagingService, - boolean requireSchemas, - BiConsumer updateCallback) - { - this.requireSchemas = requireSchemas; - this.updateCallback = updateCallback; - this.migrationCoordinator = migrationCoordinator == null ? createMigrationCoordinator(messagingService) : migrationCoordinator; - Gossiper.instance.register(this); - SchemaPushVerbHandler.instance.register(msg -> { - synchronized (this) - { - if (requestedReset == null) - applyMutations(msg.payload); - } - }); - SchemaPullVerbHandler.instance.register(msg -> { - try - { - messagingService.send(msg.responseWith(getSchemaMutations()), msg.from()); - } - catch (RuntimeException ex) - { - logger.error("Failed to send schema mutations to " + msg.from(), ex); - } - }); - } - - public synchronized void start() - { - if (StorageService.instance.isReplacing()) - onRemove(DatabaseDescriptor.getReplaceAddress()); - - SchemaKeyspace.saveSystemKeyspacesSchema(); - - migrationCoordinator.start(); - } - - @Override - public boolean waitUntilReady(Duration timeout) - { - logger.debug("Waiting for schema to be ready (max {})", timeout); - boolean schemasReceived = migrationCoordinator.awaitSchemaRequests(timeout.toMillis()); - - if (schemasReceived) - return true; - - logger.warn("There are nodes in the cluster with a different schema version than us, from which we did not merge schemas: " + - "our version: ({}), outstanding versions -> endpoints: {}. Use -D{}=true to ignore this, " + - "-D{}= to skip specific endpoints, or -D{}= to skip specific schema versions", - Schema.instance.getVersion(), - migrationCoordinator.outstandingVersions(), - CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK.getKey(), - CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_ENDPOINTS.getKey(), - CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_VERSIONS.getKey()); - - if (requireSchemas) - { - logger.error("Didn't receive schemas for all known versions within the {}. Use -D{}=true to skip this check.", - timeout, CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK.getKey()); - - return false; - } - - return true; - } - - @Override - public void onRemove(InetAddressAndPort endpoint) - { - migrationCoordinator.removeAndIgnoreEndpoint(endpoint); - } - - @Override - public void onChange(InetAddressAndPort endpoint, ApplicationState state, VersionedValue value) - { - if (state == ApplicationState.SCHEMA) - { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState != null && !Gossiper.instance.isDeadState(epState) && StorageService.instance.getTokenMetadata().isMember(endpoint)) - { - migrationCoordinator.reportEndpointVersion(endpoint, UUID.fromString(value.value)); - } - } - } - - @Override - public void onJoin(InetAddressAndPort endpoint, EndpointState epState) - { - // no-op - } - - @Override - public void beforeChange(InetAddressAndPort endpoint, EndpointState currentState, ApplicationState newStateKey, VersionedValue newValue) - { - // no-op - } - - @Override - public void onAlive(InetAddressAndPort endpoint, EndpointState state) - { - // no-op - } - - @Override - public void onDead(InetAddressAndPort endpoint, EndpointState state) - { - // no-op - } - - @Override - public void onRestart(InetAddressAndPort endpoint, EndpointState state) - { - // no-op - } - - private synchronized SchemaTransformationResult applyMutations(Collection schemaMutations) - { - // fetch the current state of schema for the affected keyspaces only - DistributedSchema before = schema; - - // apply the schema mutations - SchemaKeyspace.applyChanges(schemaMutations); - - // only compare the keyspaces affected by this set of schema mutations - Set affectedKeyspaces = SchemaKeyspace.affectedKeyspaces(schemaMutations); - - // apply the schema mutations and fetch the new versions of the altered keyspaces - Keyspaces updatedKeyspaces = SchemaKeyspace.fetchKeyspaces(affectedKeyspaces); - Set removedKeyspaces = affectedKeyspaces.stream().filter(ks -> !updatedKeyspaces.containsKeyspace(ks)).collect(Collectors.toSet()); - Keyspaces afterKeyspaces = before.getKeyspaces().withAddedOrReplaced(updatedKeyspaces).without(removedKeyspaces); - - Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before.getKeyspaces(), afterKeyspaces); - UUID version = SchemaKeyspace.calculateSchemaDigest(); - DistributedSchema after = new DistributedSchema(afterKeyspaces, version); - SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff); - - logger.info("Applying schema change due to received mutations: {}", update); - updateSchema(update, false); - return update; - } - - @Override - public synchronized SchemaTransformationResult apply(SchemaTransformation transformation, boolean local) - { - DistributedSchema before = schema; - Keyspaces afterKeyspaces = transformation.apply(before.getKeyspaces()); - Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before.getKeyspaces(), afterKeyspaces); - - if (diff.isEmpty()) - return new SchemaTransformationResult(before, before, diff); - - Collection mutations = SchemaKeyspace.convertSchemaDiffToMutations(diff, transformation.fixedTimestampMicros().orElse(FBUtilities.timestampMicros())); - SchemaKeyspace.applyChanges(mutations); - - DistributedSchema after = new DistributedSchema(afterKeyspaces, SchemaKeyspace.calculateSchemaDigest()); - SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff); - - updateSchema(update, local); - if (!local) - { - migrationCoordinator.executor.submit(() -> { - Pair, Set> endpoints = migrationCoordinator.pushSchemaMutations(mutations); - SchemaAnnouncementDiagnostics.schemaTransformationAnnounced(endpoints.left(), endpoints.right(), transformation); - }); - } - - return update; - } - - private void updateSchema(SchemaTransformationResult update, boolean local) - { - if (!update.diff.isEmpty()) - { - this.schema = update.after; - logger.debug("Schema updated: {}", update); - updateCallback.accept(update, true); - if (!local) - { - migrationCoordinator.announce(update.after.getVersion()); - } - } - else - { - logger.debug("Schema update is empty - skipping"); - } - } - - private synchronized void reload() - { - DistributedSchema before = this.schema; - DistributedSchema after = new DistributedSchema(SchemaKeyspace.fetchNonSystemKeyspaces(), SchemaKeyspace.calculateSchemaDigest()); - Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before.getKeyspaces(), after.getKeyspaces()); - SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff); - - updateSchema(update, false); - } - - @Override - public void reset(boolean local) - { - if (local) - { - reload(); - } - else - { - migrationCoordinator.reset(); - if (!migrationCoordinator.awaitSchemaRequests(CassandraRelevantProperties.MIGRATION_DELAY.getLong())) - { - logger.error("Timeout exceeded when waiting for schema from other nodes"); - } - } - } - - /** - * When clear is called the update handler will flag that the clear was requested. It means that migration - * coordinator will think that we have empty schema version and will apply whatever it receives from other nodes. - * When a first attempt to apply mutations from other node is called, it will first clear the schema and apply - * the mutations on a truncated table. The flag is then reset. - *

    - * This way the clear is postponed until we really fetch any schema we can use as a replacement. Otherwise, nothing - * will happen. We will simply reset the flag after the timeout and throw exceptions to the caller. - * - * @return - */ - @Override - public Awaitable clear() - { - synchronized (this) - { - if (requestedReset == null) - { - requestedReset = new AsyncPromise<>(); - migrationCoordinator.reset(); - } - return requestedReset; - } - } - - private UUID getSchemaVersionForCoordinator() - { - if (requestedReset != null) - return SchemaConstants.emptyVersion; - else - return schema.getVersion(); - } - - private synchronized void applyMutationsFromCoordinator(InetAddressAndPort from, Collection mutations) - { - if (requestedReset != null && !mutations.isEmpty()) - { - schema = DistributedSchema.EMPTY; - SchemaKeyspace.truncate(); - requestedReset.setSuccess(null); - requestedReset = null; - } - applyMutations(mutations); - } - - private synchronized Collection getSchemaMutations() - { - if (requestedReset != null) - return Collections.emptyList(); - else - return SchemaKeyspace.convertSchemaToMutations(); - } - - public Map> getOutstandingSchemaVersions() - { - return migrationCoordinator.outstandingVersions(); - } -} diff --git a/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java b/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java new file mode 100644 index 0000000000..9d16360c18 --- /dev/null +++ b/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java @@ -0,0 +1,284 @@ +/* + * 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.schema; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.exceptions.CasWriteTimeoutException; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Retry; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LogReader; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.transformations.cms.PreInitialize; +import org.apache.cassandra.utils.JVMStabilityInspector; + +import static org.apache.cassandra.tcm.Epoch.FIRST; + +public final class DistributedMetadataLogKeyspace +{ + private static final Logger logger = LoggerFactory.getLogger(DistributedMetadataLogKeyspace.class); + + private DistributedMetadataLogKeyspace(){} + + public static final String TABLE_NAME = "distributed_metadata_log"; + + /** + * Generation is used as a timestamp for automatic table creation on startup. + * If you make any changes to the tables below, make sure to increment the + * generation and document your change here. + * + * gen 0: original definition in 5.0 + */ + public static final long GENERATION = 0; + + public static final String LOG_TABLE_CQL = "CREATE TABLE %s.%s (" + + "period bigint," + + "current_epoch bigint static," + + "sealed boolean static," + + "epoch bigint," + + "entry_id bigint," + + "transformation blob," + + "kind text," + + "PRIMARY KEY (period, epoch))"; + + public static final TableMetadata Log = + parse(LOG_TABLE_CQL, TABLE_NAME, "Log") + .compaction(CompactionParams.twcs(ImmutableMap.of("compaction_window_unit","DAYS", + "compaction_window_size","1"))) + .build(); + + public static boolean initialize() throws IOException + { + try + { + String init = String.format("INSERT INTO %s.%s (period, epoch, current_epoch, transformation, kind, entry_id, sealed) " + + "VALUES(?, ?, ?, ?, ?, ?, false) " + + "IF NOT EXISTS", SchemaConstants.METADATA_KEYSPACE_NAME, TABLE_NAME); + UntypedResultSet result = QueryProcessor.execute(init, ConsistencyLevel.QUORUM, + Period.FIRST, FIRST.getEpoch(), FIRST.getEpoch(), + Transformation.Kind.PRE_INITIALIZE_CMS.toVersionedBytes(PreInitialize.blank()), Transformation.Kind.PRE_INITIALIZE_CMS.toString(), Entry.Id.NONE.entryId); + + return result.one().getBoolean("[applied]"); + } + catch (CasWriteTimeoutException t) + { + logger.warn("Timed out wile trying to CAS", t); + return false; + } + catch (Throwable t) + { + logger.error("Caught an exception while trying to CAS", t); + return false; + } + } + + public static boolean tryCommit(Entry.Id entryId, + Transformation transform, + Epoch previousEpoch, + Epoch nextEpoch, + long previousPeriod, + long nextPeriod, + boolean sealCurrentPeriod) + { + try + { + if (previousEpoch.is(FIRST) && !initialize()) + return false; + + // TODO get lowest supported metadata version from ClusterMetadata + ByteBuffer serializedEvent = transform.kind().toVersionedBytes(transform); + + UntypedResultSet result; + if (previousPeriod + 1 == nextPeriod || ClusterMetadataService.state() == ClusterMetadataService.State.RESET) + { + String query = String.format("INSERT INTO %s.%s (period, epoch, current_epoch, entry_id, transformation, kind, sealed) " + + "VALUES (?, ?, ?, ?, ?, ?, false) " + + "IF NOT EXISTS;", + SchemaConstants.METADATA_KEYSPACE_NAME, TABLE_NAME); + result = QueryProcessor.execute(query, ConsistencyLevel.QUORUM, + nextPeriod, nextEpoch.getEpoch(), nextEpoch.getEpoch(), entryId.entryId, serializedEvent, transform.kind().toString()); + } + else + { + assert previousPeriod == nextPeriod; + String query = String.format("UPDATE %s.%s SET current_epoch = ?, sealed = ?, entry_id = ?, transformation = ?, kind = ? " + + "WHERE period = ? AND epoch = ? " + + "IF current_epoch = ? and sealed = false;", + SchemaConstants.METADATA_KEYSPACE_NAME, TABLE_NAME); + + result = QueryProcessor.execute(query, + ConsistencyLevel.QUORUM, + nextEpoch.getEpoch(), sealCurrentPeriod, + entryId.entryId, serializedEvent, transform.kind().toString(), + previousPeriod, nextEpoch.getEpoch(), previousEpoch.getEpoch()); + } + + return result.one().getBoolean("[applied]"); + } + catch (CasWriteTimeoutException t) + { + logger.warn("Timed out wile trying to append item to the log: ", t.getMessage()); + return false; + } + catch (Throwable t) + { + logger.error("Caught an exception while trying to CAS", t); + return false; + } + } + + @VisibleForTesting + public static void truncateLogState() + { + QueryProcessor.execute(String.format("TRUNCATE %s.%s", SchemaConstants.METADATA_KEYSPACE_NAME, TABLE_NAME), ConsistencyLevel.QUORUM); + } + + + private static final LogReader localLogReader = new DistributedTableLogReader(ConsistencyLevel.NODE_LOCAL); + private static final LogReader serialLogReader = new DistributedTableLogReader(ConsistencyLevel.SERIAL); + + public static LogState getLogState(Epoch since, boolean consistentFetch) + { + return LogState.getLogState(since, ClusterMetadataService.instance().snapshotManager(), consistentFetch ? serialLogReader : localLogReader); + } + + @VisibleForTesting + public static LogState getLogState(Epoch since, LogReader logReader, MetadataSnapshots snapshots) + { + Retry retry = new Retry.Jitter(TCMMetrics.instance.fetchLogRetries); + while (!retry.reachedMax()) + { + try + { + return LogState.getLogState(since, snapshots, logReader); + } + catch (Throwable t) + { + retry.maybeSleep(); + } + } + + throw new IllegalStateException(String.format("Could not retrieve log state after %s tries.", retry.currentTries())); + } + + public static class DistributedTableLogReader implements LogReader + { + private final ConsistencyLevel consistencyLevel; + + public DistributedTableLogReader(ConsistencyLevel consistencyLevel) + { + this.consistencyLevel = consistencyLevel; + } + + @Override + public Replication getReplication(long startPeriod, Epoch since) + { + try + { + if (startPeriod == Period.EMPTY) + { + startPeriod = Period.scanLogForPeriod(Log, since); + // There shouldn't be any entries in period 0, the pre-init transform would bump it to period 1. + if (startPeriod == Period.EMPTY) + return Replication.EMPTY; + } + + long currentEpoch = since.getEpoch(); + long lastEpoch = since.getEpoch(); + + long period = startPeriod; + ImmutableList.Builder entries = new ImmutableList.Builder<>(); + + while (true) + { + boolean empty = true; + UntypedResultSet resultSet = execute(String.format("SELECT current_epoch, period, epoch, kind, transformation, entry_id, sealed FROM %s.%s WHERE period = ? AND epoch > ?", + SchemaConstants.METADATA_KEYSPACE_NAME, TABLE_NAME), + consistencyLevel, period, since.getEpoch()); + + for (UntypedResultSet.Row row : resultSet) + { + currentEpoch = row.getLong("current_epoch"); + long epochl = row.getLong("epoch"); + Epoch epoch = Epoch.create(epochl); + Transformation.Kind kind = Transformation.Kind.valueOf(row.getString("kind")); + long entryId = row.getLong("entry_id"); + Transformation transform = kind.fromVersionedBytes(row.getBlob("transformation")); + entries.add(new Entry(new Entry.Id(entryId), epoch, transform)); + + lastEpoch = currentEpoch; + empty = false; + } + + if (period != startPeriod && empty) + break; + + period++; + } + + assert currentEpoch == lastEpoch; + return new Replication(entries.build()); + } + catch (IOException t) + { + JVMStabilityInspector.inspectThrowable(t); + throw new RuntimeException(t); + } + } + }; + + private static UntypedResultSet execute(String query, ConsistencyLevel cl, Object ... params) + { + if (cl == ConsistencyLevel.NODE_LOCAL) + return QueryProcessor.executeInternal(query, params); + return QueryProcessor.execute(query, cl, params); + } + + private static TableMetadata.Builder parse(String cql, String table, String description) + { + return CreateTableStatement.parse(String.format(cql, SchemaConstants.METADATA_KEYSPACE_NAME, table), SchemaConstants.METADATA_KEYSPACE_NAME) + .id(TableId.unsafeDeterministic(SchemaConstants.METADATA_KEYSPACE_NAME, table)) + .epoch(FIRST) + .comment(description); + } + + public static KeyspaceMetadata initialMetadata(Set knownDatacenters) + { + return KeyspaceMetadata.create(SchemaConstants.METADATA_KEYSPACE_NAME, new KeyspaceParams(true, ReplicationParams.simpleMeta(1, knownDatacenters)), Tables.of(Log)); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/schema/DistributedSchema.java b/src/java/org/apache/cassandra/schema/DistributedSchema.java index ac5b15ae2e..fbdf9c1c88 100644 --- a/src/java/org/apache/cassandra/schema/DistributedSchema.java +++ b/src/java/org/apache/cassandra/schema/DistributedSchema.java @@ -18,30 +18,257 @@ package org.apache.cassandra.schema; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; import com.google.common.base.Preconditions; +import org.apache.cassandra.auth.AuthKeyspace; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tracing.TraceKeyspace; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + /** * Immutable snapshot of the current schema along with its version. */ -public class DistributedSchema +public class DistributedSchema implements MetadataValue { - public static final DistributedSchema EMPTY = new DistributedSchema(Keyspaces.none(), SchemaConstants.emptyVersion); + public static final Serializer serializer = new Serializer(); + + public static final DistributedSchema empty() + { + return new DistributedSchema(Keyspaces.none(), Epoch.EMPTY); + } + + public static DistributedSchema first() + { + return new DistributedSchema(Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Collections.singleton(DatabaseDescriptor.getLocalDataCenter()))), Epoch.FIRST); + } private final Keyspaces keyspaces; + private final Epoch epoch; private final UUID version; + private final Map keyspaceInstances = new HashMap<>(); - public DistributedSchema(Keyspaces keyspaces, UUID version) + public DistributedSchema(Keyspaces keyspaces) + { + this(keyspaces, Epoch.EMPTY); + } + + public DistributedSchema(Keyspaces keyspaces, Epoch epoch) { Objects.requireNonNull(keyspaces); - Objects.requireNonNull(version); this.keyspaces = keyspaces; - this.version = version; + this.epoch = epoch; + this.version = new UUID(0, epoch.getEpoch()); validate(); } + @Override + public DistributedSchema withLastModified(Epoch epoch) + { + return new DistributedSchema(keyspaces, epoch); + } + + @Override + public Epoch lastModified() + { + return epoch; + } + + public Keyspace getKeyspace(String keyspace) + { + return keyspaceInstances.get(keyspace); + } + + public KeyspaceMetadata getKeyspaceMetadata(String keyspace) + { + return keyspaces.get(keyspace).get(); + } + + public static DistributedSchema fromSystemTables(Keyspaces keyspaces, Set knownDatacenters) + { + if (!keyspaces.containsKeyspace(SchemaConstants.METADATA_KEYSPACE_NAME)) + keyspaces = keyspaces.withAddedOrReplaced(Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(knownDatacenters), + TraceKeyspace.metadata(), + SystemDistributedKeyspace.metadata(), + AuthKeyspace.metadata())); + return new DistributedSchema(keyspaces, Epoch.UPGRADE_GOSSIP); + } + + public void initializeKeyspaceInstances(DistributedSchema prev) + { + initializeKeyspaceInstances(prev, true); + } + + public void initializeKeyspaceInstances(DistributedSchema prev, boolean loadSSTables) + { + keyspaceInstances.putAll(prev.keyspaceInstances); + + Keyspaces.KeyspacesDiff ksDiff = Keyspaces.diff(prev.getKeyspaces(), getKeyspaces()); + + SchemaChangeNotifier schemaChangeNotifier = Schema.instance.schemaChangeNotifier(); + schemaChangeNotifier.notifyPreChanges(new SchemaTransformation.SchemaTransformationResult(prev, this, ksDiff)); + + ksDiff.dropped.forEach(metadata -> { + schemaChangeNotifier.notifyKeyspaceDropped(metadata, loadSSTables); + dropKeyspace(metadata, true); + }); + + ksDiff.created.forEach(metadata -> { + schemaChangeNotifier.notifyKeyspaceCreated(metadata); + keyspaceInstances.put(metadata.name, new Keyspace(Schema.instance, metadata, loadSSTables)); + }); + + ksDiff.altered.forEach(delta -> { + boolean initialized = Keyspace.isInitialized(); + + Keyspace keyspace = initialized ? keyspaceInstances.get(delta.before.name) : null; + if (initialized) + { + assert keyspace != null : String.format("Keyspace %s is not initialized. Initialized keyspaces: %s.", delta.before.name, keyspaceInstances.keySet()); + assert delta.before.name.equals(delta.after.name); + + // drop tables and views + delta.views.dropped.forEach(v -> dropView(keyspace, v, true)); + delta.tables.dropped.forEach(t -> dropTable(keyspace, t, true)); + + // add tables and views + delta.tables.created.forEach(t -> createTable(keyspace, t, loadSSTables)); + delta.views.created.forEach(v -> createView(keyspace, v)); + + // update tables and views + delta.tables.altered.forEach(diff -> alterTable(keyspace, diff.after)); + delta.views.altered.forEach(diff -> alterView(keyspace, diff.after)); + + schemaChangeNotifier.notifyKeyspaceAltered(delta, loadSSTables); + // deal with all added, and altered views + keyspace.viewManager.reload(keyspaces.get(keyspace.getName()).get()); + } + + //schemaChangeNotifier.notifyKeyspaceAltered(delta); + SchemaDiagnostics.keyspaceAltered(Schema.instance, delta); + }); + + // Avoid system table side effects during initialization + if (epoch.isEqualOrAfter(Epoch.FIRST)) + { + Collection mutations = SchemaKeyspace.convertSchemaDiffToMutations(ksDiff, FBUtilities.timestampMicros()); + SchemaKeyspace.applyChanges(mutations); + } + } + + public static void maybeRebuildViews(DistributedSchema prev, DistributedSchema current) + { + Keyspaces.KeyspacesDiff ksDiff = Keyspaces.diff(prev.getKeyspaces(), current.getKeyspaces()); + if (ksDiff.isEmpty() || ksDiff.altered.isEmpty()) + return; + ksDiff.altered.forEach(delta -> { + if (delta.views.isEmpty()) + return; + boolean initialized = Keyspace.isInitialized(); + Keyspace keyspace = initialized ? current.keyspaceInstances.get(delta.after.name) : null; + if (keyspace != null) + keyspace.viewManager.buildViews(); + }); + + } + + private void dropView(Keyspace keyspace, ViewMetadata metadata, boolean dropData) + { + keyspace.viewManager.dropView(metadata.name()); + dropTable(keyspace, metadata.metadata, dropData); + } + + // TODO: handle drops after snapshots + private void dropKeyspace(KeyspaceMetadata keyspaceMetadata, boolean dropData) + { + SchemaDiagnostics.keyspaceDropping(Schema.instance, keyspaceMetadata); + + boolean initialized = Keyspace.isInitialized(); + Keyspace keyspace = initialized ? Keyspace.open(keyspaceMetadata.name) : null; + if (initialized) + { + if (keyspace == null) + return; + + keyspaceMetadata.views.forEach(v -> dropView(keyspace, v, dropData)); + keyspaceMetadata.tables.forEach(t -> dropTable(keyspace, t, dropData)); + + // remove the keyspace from the static instances + Keyspace unloadedKeyspace = keyspaceInstances.remove(keyspaceMetadata.name); + unloadedKeyspace.unload(true); + SchemaDiagnostics.metadataRemoved(Schema.instance, keyspaceMetadata); + assert unloadedKeyspace == keyspace; + + Keyspace.writeOrder.awaitNewBarrier(); + } + else + { + keyspace.unload(true); + SchemaDiagnostics.metadataRemoved(Schema.instance, keyspaceMetadata); + } + + SchemaDiagnostics.keyspaceDropped(Schema.instance, keyspaceMetadata); + } + /** + * + * @param keyspace + * @param metadata + */ + private void dropTable(Keyspace keyspace, TableMetadata metadata, boolean dropData) + { + SchemaDiagnostics.tableDropping(Schema.instance, metadata); + keyspace.dropCf(metadata.id, dropData); + SchemaDiagnostics.tableDropped(Schema.instance, metadata); + } + + private void createTable(Keyspace keyspace, TableMetadata table, boolean loadSSTables) + { + SchemaDiagnostics.tableCreating(Schema.instance, table); + keyspace.initCf(table, loadSSTables); + SchemaDiagnostics.tableCreated(Schema.instance, table); + } + + private void createView(Keyspace keyspace, ViewMetadata view) + { + SchemaDiagnostics.tableCreating(Schema.instance, view.metadata); + keyspace.initCf(view.metadata, true); + SchemaDiagnostics.tableCreated(Schema.instance, view.metadata); + } + + private void alterTable(Keyspace keyspace, TableMetadata updated) + { + SchemaDiagnostics.tableAltering(Schema.instance, updated); + keyspace.getColumnFamilyStore(updated.name).reload(updated); + SchemaDiagnostics.tableAltered(Schema.instance, updated); + } + + private void alterView(Keyspace keyspace, ViewMetadata updated) + { + SchemaDiagnostics.tableAltering(Schema.instance, updated.metadata); + keyspace.getColumnFamilyStore(updated.name()).reload(updated.metadata); + SchemaDiagnostics.tableAltered(Schema.instance, updated.metadata); + } + public Keyspaces getKeyspaces() { return keyspaces; @@ -49,7 +276,7 @@ public class DistributedSchema public boolean isEmpty() { - return SchemaConstants.emptyVersion.equals(version); + return epoch.is(Epoch.EMPTY); } public UUID getVersion() @@ -94,4 +321,46 @@ public class DistributedSchema ksm.userFunctions.forEach(f -> Preconditions.checkArgument(f.name().keyspace.equals(ksm.name), "Function %s points to keyspace %s while defined in keyspace %s", f.name().name, f.name().keyspace, ksm.name)); }); } + + public static class Serializer implements MetadataSerializer + { + public void serialize(DistributedSchema t, DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(t.epoch, out, version); + out.writeInt(t.keyspaces.size()); + for (KeyspaceMetadata ksm : t.keyspaces) + KeyspaceMetadata.serializer.serialize(ksm, out, version); + } + + public DistributedSchema deserialize(DataInputPlus in, Version version) throws IOException + { + Epoch basedOnEpoch = Epoch.serializer.deserialize(in, version); + int ksCount = in.readInt(); + List ksms = new ArrayList<>(ksCount); + for (int i = 0; i < ksCount; i++) + ksms.add(KeyspaceMetadata.serializer.deserialize(in, version)); + + return new DistributedSchema(Keyspaces.of(ksms.toArray(new KeyspaceMetadata[ksCount])), basedOnEpoch); + } + + public long serializedSize(DistributedSchema t, Version version) + { + long size = Epoch.serializer.serializedSize(t.epoch, version); + size += sizeof(t.keyspaces.size()); + for (KeyspaceMetadata ksm : t.keyspaces) + size += KeyspaceMetadata.serializer.serializedSize(ksm, version); + return size; + } + } + + @Override + public String toString() + { + return "DistributedSchema{" + + "keyspaces=" + keyspaces + + ", epoch=" + epoch + + ", version=" + version + + ", keyspaceInstances=" + keyspaceInstances + + '}'; + } } diff --git a/src/java/org/apache/cassandra/schema/DroppedColumn.java b/src/java/org/apache/cassandra/schema/DroppedColumn.java index 90dfe651f7..0cd001fa54 100644 --- a/src/java/org/apache/cassandra/schema/DroppedColumn.java +++ b/src/java/org/apache/cassandra/schema/DroppedColumn.java @@ -17,11 +17,23 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; + import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + + public final class DroppedColumn { + public static final Serializer serializer = new Serializer(); + public final ColumnMetadata column; public final long droppedTime; // drop timestamp, in microseconds, yet with millisecond granularity @@ -56,4 +68,26 @@ public final class DroppedColumn { return MoreObjects.toStringHelper(this).add("column", column).add("droppedTime", droppedTime).toString(); } + + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(DroppedColumn t, DataOutputPlus out, Version version) throws IOException + { + out.writeLong(t.droppedTime); + ColumnMetadata.serializer.serialize(t.column, out, version); + } + + public DroppedColumn deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + long droppedTime = in.readLong(); + ColumnMetadata column = ColumnMetadata.serializer.deserialize(in, types, functions, version); + return new DroppedColumn(column, droppedTime); + } + + public long serializedSize(DroppedColumn t, Version version) + { + return sizeof(t.droppedTime) + + ColumnMetadata.serializer.serializedSize(t.column, version); + } + } } diff --git a/src/java/org/apache/cassandra/schema/IndexMetadata.java b/src/java/org/apache/cassandra/schema/IndexMetadata.java index 795abad984..6667846e85 100644 --- a/src/java/org/apache/cassandra/schema/IndexMetadata.java +++ b/src/java/org/apache/cassandra/schema/IndexMetadata.java @@ -44,9 +44,12 @@ import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.UUIDSerializer; +import static org.apache.cassandra.db.TypeSizes.sizeof; + /** * An immutable representation of secondary index metadata. */ @@ -59,6 +62,7 @@ public final class IndexMetadata public static final Serializer serializer = new Serializer(); + public static final MetadataSerializer metadataSerializer = new MetadataSerializer(); /** * A mapping of user-friendly index names to their fully qualified index class names. @@ -332,4 +336,41 @@ public final class IndexMetadata return UUIDSerializer.serializer.serializedSize(metadata.id, version); } } + + public static class MetadataSerializer implements org.apache.cassandra.tcm.serialization.MetadataSerializer + { + public void serialize(IndexMetadata t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.name); + out.writeUTF(t.kind.name()); + out.writeInt(t.options.size()); + for (Map.Entry entry : t.options.entrySet()) + { + out.writeUTF(entry.getKey()); + out.writeUTF(entry.getValue()); + } + } + + public IndexMetadata deserialize(DataInputPlus in, Version version) throws IOException + { + String name = in.readUTF(); + Kind kind = Kind.valueOf(in.readUTF()); + int size = in.readInt(); + + Map options = Maps.newHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) + options.put(in.readUTF(), in.readUTF()); + return new IndexMetadata(name, options, kind); + } + + public long serializedSize(IndexMetadata t, Version version) + { + int size = sizeof(t.name) + sizeof(t.kind.name()) + sizeof(t.options.size()); + + for (Map.Entry entry : t.options.entrySet()) + size = size + sizeof(entry.getKey()) + sizeof(entry.getValue()); + + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/Indexes.java b/src/java/org/apache/cassandra/schema/Indexes.java index 2e95779674..0282cfc666 100644 --- a/src/java/org/apache/cassandra/schema/Indexes.java +++ b/src/java/org/apache/cassandra/schema/Indexes.java @@ -17,14 +17,21 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.*; import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + import static java.lang.String.format; import static com.google.common.collect.Iterables.filter; +import static org.apache.cassandra.db.TypeSizes.sizeof; /** * For backwards compatibility, in the first instance an IndexMetadata must have @@ -37,6 +44,8 @@ import static com.google.common.collect.Iterables.filter; */ public final class Indexes implements Iterable { + public static final Serializer serializer = new Serializer(); + private final ImmutableMap indexesByName; private final ImmutableMap indexesById; @@ -215,4 +224,31 @@ public final class Indexes implements Iterable return this; } } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Indexes t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.size()); + for (IndexMetadata im : t.indexesById.values()) + IndexMetadata.metadataSerializer.serialize(im, out, version); + } + + public Indexes deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + Indexes.Builder builder = Indexes.builder(); + for (int i = 0; i < size; i++) + builder.add(IndexMetadata.metadataSerializer.deserialize(in, version)); + return builder.build(); + } + + public long serializedSize(Indexes t, Version version) + { + int size = sizeof(t.size()); + for (IndexMetadata metadata : t.indexesById.values()) + size += IndexMetadata.metadataSerializer.serializedSize(metadata, version); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java index 5d62cf8b43..14ca49f53a 100644 --- a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java +++ b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -28,7 +29,6 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.Iterables; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.SchemaElement; import org.apache.cassandra.cql3.functions.Function; @@ -36,22 +36,28 @@ import org.apache.cassandra.cql3.functions.UDAggregate; import org.apache.cassandra.cql3.functions.UDFunction; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.schema.UserFunctions.FunctionsDiff; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.schema.Tables.TablesDiff; import org.apache.cassandra.schema.Types.TypesDiff; import org.apache.cassandra.schema.Views.ViewsDiff; -import org.apache.cassandra.service.StorageService; - -import static java.lang.String.format; import static com.google.common.collect.Iterables.any; +import static java.lang.String.format; +import static org.apache.cassandra.db.TypeSizes.sizeof; /** * An immutable representation of keyspace metadata (name, params, tables, types, and functions). */ public final class KeyspaceMetadata implements SchemaElement { + public static final Serializer serializer = new Serializer(); + public enum Kind { REGULAR, VIRTUAL @@ -59,21 +65,23 @@ public final class KeyspaceMetadata implements SchemaElement public final String name; public final Kind kind; + public final AbstractReplicationStrategy replicationStrategy; public final KeyspaceParams params; public final Tables tables; public final Views views; public final Types types; public final UserFunctions userFunctions; - private KeyspaceMetadata(String name, Kind kind, KeyspaceParams params, Tables tables, Views views, Types types, UserFunctions functions) + private KeyspaceMetadata(String keyspaceName, Kind kind, KeyspaceParams params, Tables tables, Views views, Types types, UserFunctions functions) { - this.name = name; + this.name = keyspaceName; this.kind = kind; this.params = params; this.tables = tables; this.views = views; this.types = types; this.userFunctions = functions; + this.replicationStrategy = AbstractReplicationStrategy.createReplicationStrategy(keyspaceName, params.replication); } public static KeyspaceMetadata create(String name, KeyspaceParams params) @@ -214,6 +222,15 @@ public final class KeyspaceMetadata implements SchemaElement return Optional.empty(); } + public Optional getIndexMetadata(String indexName) + { + TableMetadata metadata = tables.indexTables().get(indexName); + if (metadata != null) + return Optional.of(metadata); + + return Optional.empty(); + } + @Override public int hashCode() { @@ -315,7 +332,7 @@ public final class KeyspaceMetadata implements SchemaElement return builder.toString(); } - public void validate() + public void validate(ClusterMetadata metadata) { if (!SchemaConstants.isValidName(name)) { @@ -325,8 +342,7 @@ public final class KeyspaceMetadata implements SchemaElement name)); } - params.validate(name, null); - + params.validate(name, null, metadata); tablesAndViews().forEach(TableMetadata::validate); Set indexNames = new HashSet<>(); @@ -342,15 +358,6 @@ public final class KeyspaceMetadata implements SchemaElement } } - public AbstractReplicationStrategy createReplicationStrategy() - { - return AbstractReplicationStrategy.createReplicationStrategy(name, - params.replication.klass, - StorageService.instance.getTokenMetadata(), - DatabaseDescriptor.getEndpointSnitch(), - params.replication.options); - } - static Optional diff(KeyspaceMetadata before, KeyspaceMetadata after) { return KeyspaceDiff.diff(before, after); @@ -428,4 +435,38 @@ public final class KeyspaceMetadata implements SchemaElement '}'; } } + + public static class Serializer implements MetadataSerializer + { + public void serialize(KeyspaceMetadata t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.name); + Types.serializer.serialize(t.types, out, version); + KeyspaceParams.serializer.serialize(t.params, out, version); + UserFunctions.serializer.serialize(t.userFunctions, out, version); + Tables.serializer.serialize(t.tables, out, version); + Views.serializer.serialize(t.views, out, version); + } + + public KeyspaceMetadata deserialize(DataInputPlus in, Version version) throws IOException + { + String name = in.readUTF(); + Types types = Types.serializer.deserialize(name, in, version); + KeyspaceParams params = KeyspaceParams.serializer.deserialize(in, version); + UserFunctions functions = UserFunctions.serializer.deserialize(in, types, version); + Tables tables = Tables.serializer.deserialize(in, types, functions, version); + Views views = Views.serializer.deserialize(in, types, functions, version); + return KeyspaceMetadata.create(name, params, tables, views, types, functions); + } + + public long serializedSize(KeyspaceMetadata t, Version version) + { + return sizeof(t.name) + + Types.serializer.serializedSize(t.types, version) + + KeyspaceParams.serializer.serializedSize(t.params, version) + + UserFunctions.serializer.serializedSize(t.userFunctions, version) + + Tables.serializer.serializedSize(t.tables, version) + + Views.serializer.serializedSize(t.views, version); + } + } } diff --git a/src/java/org/apache/cassandra/schema/KeyspaceParams.java b/src/java/org/apache/cassandra/schema/KeyspaceParams.java index 539993e2b3..9cfaaa17f6 100644 --- a/src/java/org/apache/cassandra/schema/KeyspaceParams.java +++ b/src/java/org/apache/cassandra/schema/KeyspaceParams.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.Map; import com.google.common.annotations.VisibleForTesting; @@ -24,12 +25,20 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; /** * An immutable class representing keyspace parameters (durability and replication). */ public final class KeyspaceParams { + public static final Serializer serializer = new Serializer(); + public static final boolean DEFAULT_DURABLE_WRITES = true; /** @@ -91,9 +100,9 @@ public final class KeyspaceParams return new KeyspaceParams(true, ReplicationParams.nts(args)); } - public void validate(String name, ClientState state) + public void validate(String name, ClientState state, ClusterMetadata metadata) { - replication.validate(name, state); + replication.validate(name, state, metadata); } @Override @@ -124,4 +133,26 @@ public final class KeyspaceParams .add(Option.REPLICATION.toString(), replication) .toString(); } + + public static class Serializer implements MetadataSerializer + { + public void serialize(KeyspaceParams t, DataOutputPlus out, Version version) throws IOException + { + ReplicationParams.serializer.serialize(t.replication, out, version); + out.writeBoolean(t.durableWrites); + } + + public KeyspaceParams deserialize(DataInputPlus in, Version version) throws IOException + { + ReplicationParams params = ReplicationParams.serializer.deserialize(in, version); + boolean durableWrites = in.readBoolean(); + return new KeyspaceParams(durableWrites, params); + } + + public long serializedSize(KeyspaceParams t, Version version) + { + return ReplicationParams.serializer.serializedSize(t.replication, version) + + TypeSizes.sizeof(t.durableWrites); + } + } } diff --git a/src/java/org/apache/cassandra/schema/Keyspaces.java b/src/java/org/apache/cassandra/schema/Keyspaces.java index e9bd92c7c5..3f5431da1a 100644 --- a/src/java/org/apache/cassandra/schema/Keyspaces.java +++ b/src/java/org/apache/cassandra/schema/Keyspaces.java @@ -19,7 +19,9 @@ package org.apache.cassandra.schema; import java.util.Collection; import java.util.Iterator; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; @@ -28,23 +30,21 @@ import javax.annotation.Nullable; import com.google.common.collect.*; import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.btree.BTreeMap; public final class Keyspaces implements Iterable { - private static final Keyspaces NONE = builder().build(); + public static final Keyspaces NONE = new Keyspaces(BTreeMap.empty(), BTreeMap.empty()); - private final ImmutableMap keyspaces; - private final ImmutableMap tables; + private final BTreeMap keyspaces; + private final BTreeMap tables; - private Keyspaces(Builder builder) + private Keyspaces(BTreeMap keyspaces, + BTreeMap tables) { - keyspaces = builder.keyspaces.build(); - tables = builder.tables.build(); - } - - public static Builder builder() - { - return new Builder(); + this.keyspaces = keyspaces; + this.tables = tables; } public static Keyspaces none() @@ -54,7 +54,28 @@ public final class Keyspaces implements Iterable public static Keyspaces of(KeyspaceMetadata... keyspaces) { - return builder().add(keyspaces).build(); + BTreeMap newKeyspaces = BTreeMap.empty(); + BTreeMap newTables = BTreeMap.empty(); + for (KeyspaceMetadata ks : keyspaces) + { + newKeyspaces = newKeyspaces.with(ks.name, ks); + newTables = withTablesViews(newTables, ks); + } + return new Keyspaces(newKeyspaces, newTables); + } + + public Keyspaces with(KeyspaceMetadata ks) + { + assert !keyspaces.containsKey(ks.name) : "Keyspace already exists: "+ks.name; + return new Keyspaces(keyspaces.with(ks.name, ks), withTablesViews(tables, ks)); + } + + public Keyspaces with(Iterable kss) + { + Keyspaces k = this; + for (KeyspaceMetadata ks : kss) + k = k.with(ks); + return k; } public Iterator iterator() @@ -67,7 +88,7 @@ public final class Keyspaces implements Iterable return keyspaces.values().stream(); } - public ImmutableSet names() + public Set names() { return keyspaces.keySet(); } @@ -107,9 +128,19 @@ public final class Keyspaces implements Iterable public Keyspaces filter(Predicate predicate) { - Builder builder = builder(); - stream().filter(predicate).forEach(builder::add); - return builder.build(); + BTreeMap kss = keyspaces; + BTreeMap tbls = tables; + // todo: bulk removals from BTreeMap + for (Map.Entry entry : keyspaces.entrySet()) + { + if (!predicate.test(entry.getValue())) + { + kss = kss.without(entry.getKey()); + tbls = withoutKsTablesViews(tbls, entry.getValue()); + } + } + + return new Keyspaces(kss, tbls); } /** @@ -131,9 +162,29 @@ public final class Keyspaces implements Iterable public Keyspaces withAddedOrUpdated(KeyspaceMetadata keyspace) { - return builder().add(Iterables.filter(this, k -> !k.name.equals(keyspace.name))) - .add(keyspace) - .build(); + Keyspaces updated = keyspaces.containsKey(keyspace.name) ? without(keyspace.name) : this; + return updated.with(keyspace); + } + + private static BTreeMap withoutKsTablesViews(BTreeMap tables, KeyspaceMetadata ks) + { + // todo: bulk ops + BTreeMap tbls = tables; + for (TableMetadata table : ks.tables) + tbls = tbls.without(table.id); + for (ViewMetadata view : ks.views) + tbls = tbls.without(view.metadata.id); + return tbls; + } + + private static BTreeMap withTablesViews(BTreeMap tables, KeyspaceMetadata ks) + { + BTreeMap tbls = tables; + for (TableMetadata table : ks.tables) + tbls = tbls.with(table.id, table); + for (ViewMetadata view : ks.views) + tbls = tbls.with(view.metadata.id, view.metadata); + return tbls; } /** @@ -149,9 +200,7 @@ public final class Keyspaces implements Iterable */ public Keyspaces withAddedOrReplaced(KeyspaceMetadata keyspace) { - return builder().add(Iterables.filter(this, k -> !k.name.equals(keyspace.name))) - .add(keyspace) - .build(); + return filter(ksm -> !ksm.name.equals(keyspace.name)).with(keyspace); } /** @@ -162,14 +211,17 @@ public final class Keyspaces implements Iterable */ public Keyspaces withAddedOrReplaced(Keyspaces keyspaces) { - return builder().add(Iterables.filter(this, k -> !keyspaces.containsKeyspace(k.name))) - .add(keyspaces) - .build(); + Keyspaces kss = this; + + for (KeyspaceMetadata ksm : keyspaces) + kss = kss.withAddedOrReplaced(ksm); + return kss; } public void validate() { - keyspaces.values().forEach(KeyspaceMetadata::validate); + ClusterMetadata metadata = ClusterMetadata.current(); + keyspaces.values().forEach((ksm) -> ksm.validate(metadata)); } @Override @@ -195,44 +247,6 @@ public final class Keyspaces implements Iterable return keyspaces.size(); } - public static final class Builder - { - private final ImmutableMap.Builder keyspaces = new ImmutableMap.Builder<>(); - private final ImmutableMap.Builder tables = new ImmutableMap.Builder<>(); - - private Builder() - { - } - - public Keyspaces build() - { - return new Keyspaces(this); - } - - public Builder add(KeyspaceMetadata keyspace) - { - keyspaces.put(keyspace.name, keyspace); - - keyspace.tables.forEach(t -> tables.put(t.id, t)); - keyspace.views.forEach(v -> tables.put(v.metadata.id, v.metadata)); - - return this; - } - - public Builder add(KeyspaceMetadata... keyspaces) - { - for (KeyspaceMetadata keyspace : keyspaces) - add(keyspace); - return this; - } - - public Builder add(Iterable keyspaces) - { - keyspaces.forEach(this::add); - return this; - } - } - public static KeyspacesDiff diff(Keyspaces before, Keyspaces after) { return KeyspacesDiff.diff(before, after); diff --git a/src/java/org/apache/cassandra/schema/MigrationCoordinator.java b/src/java/org/apache/cassandra/schema/MigrationCoordinator.java deleted file mode 100644 index 562489966a..0000000000 --- a/src/java/org/apache/cassandra/schema/MigrationCoordinator.java +++ /dev/null @@ -1,761 +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.schema; - -import java.lang.management.ManagementFactory; -import java.net.UnknownHostException; -import java.time.Duration; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.LongSupplier; -import java.util.function.Supplier; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.concurrent.FutureTask; -import org.apache.cassandra.concurrent.ScheduledExecutors; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.db.Mutation; -import org.apache.cassandra.exceptions.RequestFailureReason; -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.InetAddressAndPort; -import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.net.NoPayload; -import org.apache.cassandra.net.RequestCallback; -import org.apache.cassandra.net.Verb; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.NoSpamLogger; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.Simulate; -import org.apache.cassandra.utils.concurrent.Future; -import org.apache.cassandra.utils.concurrent.ImmediateFuture; -import org.apache.cassandra.utils.concurrent.WaitQueue; - -import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_ENDPOINTS; -import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_VERSIONS; -import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_PULL_INTERVAL_MS; -import static org.apache.cassandra.net.Verb.SCHEMA_PUSH_REQ; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; -import static org.apache.cassandra.utils.Simulate.With.MONITORS; -import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue; - -/** - * Migration coordinator is responsible for tracking schema versions on various nodes and, if needed, synchronize the - * schema. It performs periodic checks and if there is a schema version mismatch between the current node and the other - * node, it pulls the schema and applies the changes locally through the callback. - * - * In particular the Migration Coordinator keeps track of all schema versions reported from each node in the cluster. - * As long as a certain version is advertised by some node, it is being tracked. As long as a version is tracked, - * the migration coordinator tries to fetch it by its periodic job. - * - * It works in close cooperation with {@link DefaultSchemaUpdateHandler} which is responsible for maintaining local - * schema metadata stored in {@link SchemaKeyspace}. - */ -@Simulate(with = MONITORS) -public class MigrationCoordinator -{ - private static final Logger logger = LoggerFactory.getLogger(MigrationCoordinator.class); - private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(MigrationCoordinator.logger, 1, TimeUnit.MINUTES); - private static final Future FINISHED_FUTURE = ImmediateFuture.success(null); - - private static LongSupplier getUptimeFn = () -> ManagementFactory.getRuntimeMXBean().getUptime(); - - @VisibleForTesting - public static void setUptimeFn(LongSupplier supplier) - { - getUptimeFn = supplier; - } - - private static final int MIGRATION_DELAY_IN_MS = CassandraRelevantProperties.MIGRATION_DELAY.getInt(); - public static final int MAX_OUTSTANDING_VERSION_REQUESTS = 3; - - private static ImmutableSet getIgnoredVersions() - { - String s = IGNORED_SCHEMA_CHECK_VERSIONS.getString(); - if (s == null || s.isEmpty()) - return ImmutableSet.of(); - - ImmutableSet.Builder versions = ImmutableSet.builder(); - for (String version : s.split(",")) - { - versions.add(UUID.fromString(version)); - } - - return versions.build(); - } - - private static final Set IGNORED_VERSIONS = getIgnoredVersions(); - - private static Set getIgnoredEndpoints() - { - Set endpoints = new HashSet<>(); - - String s = IGNORED_SCHEMA_CHECK_ENDPOINTS.getString(); - if (s == null || s.isEmpty()) - return endpoints; - - for (String endpoint : s.split(",")) - { - try - { - endpoints.add(InetAddressAndPort.getByName(endpoint)); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } - - return endpoints; - } - - static class VersionInfo - { - final UUID version; - - /** - * The set of endpoints containing this schema version - */ - final Set endpoints = Sets.newConcurrentHashSet(); - /** - * The set of endpoints from which we are already fetching the schema - */ - final Set outstandingRequests = Sets.newConcurrentHashSet(); - /** - * The queue of endpoints from which we are going to fetch the schema - */ - final Deque requestQueue = new ArrayDeque<>(); - - /** - * Threads waiting for schema synchronization are waiting until this object is signalled - */ - private final WaitQueue waitQueue = newWaitQueue(); - - /** - * Whether this schema version have been received - */ - volatile boolean receivedSchema; - - VersionInfo(UUID version) - { - this.version = version; - } - - WaitQueue.Signal register() - { - return waitQueue.register(); - } - - void markReceived() - { - if (receivedSchema) - return; - - receivedSchema = true; - waitQueue.signalAll(); - } - - boolean wasReceived() - { - return receivedSchema; - } - - @Override - public String toString() - { - return "VersionInfo{" + - "version=" + version + - ", outstandingRequests=" + outstandingRequests + - ", requestQueue=" + requestQueue + - ", waitQueue.waiting=" + waitQueue.getWaiting() + - ", receivedSchema=" + receivedSchema + - '}'; - } - } - - private final Map versionInfo = new HashMap<>(); - private final Map endpointVersions = new HashMap<>(); - private final Set ignoredEndpoints = getIgnoredEndpoints(); - private final ScheduledExecutorService periodicCheckExecutor; - private final MessagingService messagingService; - private final AtomicReference> periodicPullTask = new AtomicReference<>(); - private final int maxOutstandingVersionRequests; - private final Gossiper gossiper; - private final Supplier schemaVersion; - private final BiConsumer> schemaUpdateCallback; - - final ExecutorPlus executor; - - /** - * Creates but does not start migration coordinator instance. - * @param messagingService messaging service instance used to communicate with other nodes for pulling schema - * and pushing changes - * @param periodicCheckExecutor executor on which the periodic checks are scheduled - */ - MigrationCoordinator(MessagingService messagingService, - ExecutorPlus executor, - ScheduledExecutorService periodicCheckExecutor, - int maxOutstandingVersionRequests, - Gossiper gossiper, - Supplier schemaVersionSupplier, - BiConsumer> schemaUpdateCallback) - { - this.messagingService = messagingService; - this.executor = executor; - this.periodicCheckExecutor = periodicCheckExecutor; - this.maxOutstandingVersionRequests = maxOutstandingVersionRequests; - this.gossiper = gossiper; - this.schemaVersion = schemaVersionSupplier; - this.schemaUpdateCallback = schemaUpdateCallback; - } - - void start() - { - long interval = SCHEMA_PULL_INTERVAL_MS.getLong(); - logger.info("Starting migration coordinator and scheduling pulling schema versions every {}", Duration.ofMillis(interval)); - announce(schemaVersion.get()); - periodicPullTask.updateAndGet(curTask -> curTask == null - ? periodicCheckExecutor.scheduleWithFixedDelay(this::pullUnreceivedSchemaVersions, interval, interval, TimeUnit.MILLISECONDS) - : curTask); - } - - private synchronized void pullUnreceivedSchemaVersions() - { - logger.debug("Pulling unreceived schema versions..."); - for (VersionInfo info : versionInfo.values()) - { - if (info.wasReceived() || info.outstandingRequests.size() > 0) - { - logger.trace("Skipping pull of schema {} because it has been already recevied, or it is being received ({})", info.version, info); - continue; - } - - maybePullSchema(info); - } - } - - private synchronized Future maybePullSchema(VersionInfo info) - { - if (info.endpoints.isEmpty() || info.wasReceived() || !shouldPullSchema(info.version)) - { - logger.trace("Not pulling schema {} because it was received, there is no endpoint to provide it, or we should not pull it ({})", info.version, info); - return FINISHED_FUTURE; - } - - if (info.outstandingRequests.size() >= maxOutstandingVersionRequests) - { - logger.trace("Not pulling schema {} because the number of outstanding requests has been exceeded ({} >= {})", info.version, info.outstandingRequests.size(), maxOutstandingVersionRequests); - return FINISHED_FUTURE; - } - - for (int i = 0, isize = info.requestQueue.size(); i < isize; i++) - { - InetAddressAndPort endpoint = info.requestQueue.remove(); - if (!info.endpoints.contains(endpoint)) - { - logger.trace("Skipping request of schema {} from {} because the endpoint does not have that schema any longer", info.version, endpoint); - continue; - } - - if (shouldPullFromEndpoint(endpoint) && info.outstandingRequests.add(endpoint)) - { - return scheduleSchemaPull(endpoint, info); - } - else - { - // return to queue - logger.trace("Could not pull schema {} from {} - the request will be added back to the queue", info.version, endpoint); - info.requestQueue.offer(endpoint); - } - } - - // no suitable endpoints were found, check again in a minute, the periodic task will pick it up - return FINISHED_FUTURE; - } - - synchronized Map> outstandingVersions() - { - HashMap> map = new HashMap<>(); - for (VersionInfo info : versionInfo.values()) - if (!info.wasReceived()) - map.put(info.version, ImmutableSet.copyOf(info.endpoints)); - return map; - } - - @VisibleForTesting - VersionInfo getVersionInfoUnsafe(UUID version) - { - return versionInfo.get(version); - } - - private boolean shouldPullSchema(UUID version) - { - UUID localSchemaVersion = schemaVersion.get(); - if (localSchemaVersion == null) - { - logger.debug("Not pulling schema {} because the local schama version is not known yet", version); - return false; - } - - if (localSchemaVersion.equals(version)) - { - logger.debug("Not pulling schema {} because it is the same as the local schema", version); - return false; - } - - return true; - } - - private boolean shouldPullFromEndpoint(InetAddressAndPort endpoint) - { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - { - logger.trace("Not pulling schema from local endpoint"); - return false; - } - - EndpointState state = gossiper.getEndpointStateForEndpoint(endpoint); - if (state == null) - { - logger.trace("Not pulling schema from endpoint {} because its state is unknown", endpoint); - return false; - } - - VersionedValue releaseVersionValue = state.getApplicationState(ApplicationState.RELEASE_VERSION); - if (releaseVersionValue == null) - return false; - final String releaseVersion = releaseVersionValue.value; - final String ourMajorVersion = FBUtilities.getReleaseVersionMajor(); - - if (!releaseVersion.startsWith(ourMajorVersion)) - { - logger.debug("Not pulling schema from {} because release version in Gossip is not major version {}, it is {}", - endpoint, ourMajorVersion, releaseVersion); - return false; - } - - if (!messagingService.versions.knows(endpoint)) - { - logger.debug("Not pulling schema from {} because their messaging version is unknown", endpoint); - return false; - } - - if (messagingService.versions.getRaw(endpoint) != MessagingService.current_version) - { - logger.debug("Not pulling schema from {} because their schema format is incompatible", endpoint); - return false; - } - - if (gossiper.isGossipOnlyMember(endpoint)) - { - logger.debug("Not pulling schema from {} because it's a gossip only member", endpoint); - return false; - } - return true; - } - - private boolean shouldPullImmediately(InetAddressAndPort endpoint, UUID version) - { - UUID localSchemaVersion = schemaVersion.get(); - if (SchemaConstants.emptyVersion.equals(localSchemaVersion) || getUptimeFn.getAsLong() < MIGRATION_DELAY_IN_MS) - { - // If we think we may be bootstrapping or have recently started, submit MigrationTask immediately - logger.debug("Immediately submitting migration task for {}, " + - "schema versions: local={}, remote={}", - endpoint, - DistributedSchema.schemaVersionToString(localSchemaVersion), - DistributedSchema.schemaVersionToString(version)); - return true; - } - return false; - } - - /** - * If a previous schema update brought our version the same as the incoming schema, don't apply it - */ - private synchronized boolean shouldApplySchemaFor(VersionInfo info) - { - if (info.wasReceived()) - return false; - return !Objects.equals(schemaVersion.get(), info.version); - } - - synchronized Future reportEndpointVersion(InetAddressAndPort endpoint, UUID version) - { - logger.debug("Reported schema {} at endpoint {}", version, endpoint); - if (ignoredEndpoints.contains(endpoint) || IGNORED_VERSIONS.contains(version)) - { - endpointVersions.remove(endpoint); - removeEndpointFromVersion(endpoint, null); - logger.debug("Discarding endpoint {} or schema {} because either endpoint or schema version were marked as ignored", endpoint, version); - return FINISHED_FUTURE; - } - - UUID current = endpointVersions.put(endpoint, version); - if (current != null && current.equals(version)) - { - logger.trace("Skipping report of schema {} from {} because we already know that", version, endpoint); - return FINISHED_FUTURE; - } - - VersionInfo info = versionInfo.computeIfAbsent(version, VersionInfo::new); - if (Objects.equals(schemaVersion.get(), version)) - { - info.markReceived(); - logger.trace("Schema {} from {} has been marked as recevied because it is equal the local schema", version, endpoint); - } - else - { - info.requestQueue.addFirst(endpoint); - } - info.endpoints.add(endpoint); - logger.trace("Added endpoint {} to schema {}: {}", endpoint, info.version, info); - - // disassociate this endpoint from its (now) previous schema version - removeEndpointFromVersion(endpoint, current); - - return maybePullSchema(info); - } - - private synchronized void removeEndpointFromVersion(InetAddressAndPort endpoint, UUID version) - { - if (version == null) - return; - - VersionInfo info = versionInfo.get(version); - - if (info == null) - return; - - info.endpoints.remove(endpoint); - logger.trace("Removed endpoint {} from schema {}: {}", endpoint, version, info); - if (info.endpoints.isEmpty()) - { - info.waitQueue.signalAll(); - versionInfo.remove(version); - logger.trace("Removed schema info: {}", info); - } - } - - private void clearVersionsInfo() - { - Iterator> it = versionInfo.entrySet().iterator(); - while (it.hasNext()) - { - Map.Entry entry = it.next(); - it.remove(); - entry.getValue().waitQueue.signal(); - } - } - - private void reportCurrentSchemaVersionOnEndpoint(InetAddressAndPort endpoint) - { - if (FBUtilities.getBroadcastAddressAndPort().equals(endpoint)) - { - reportEndpointVersion(endpoint, schemaVersion.get()); - } - else - { - EndpointState state = gossiper.getEndpointStateForEndpoint(endpoint); - if (state != null) - { - UUID v = state.getSchemaVersion(); - if (v != null) - { - reportEndpointVersion(endpoint, v); - } - } - } - } - - /** - * Resets the migration coordinator by notifying all waiting threads and removing all the existing version info. - * Then, it is populated with the information about schema versions on different endpoints provided by Gossiper. - * Each version is marked as unreceived so the migration coordinator will start pulling schemas from other nodes. - */ - synchronized void reset() - { - logger.info("Resetting migration coordinator..."); - - // clear all the managed information - this.endpointVersions.clear(); - clearVersionsInfo(); - - // now report again the versions we are aware of - gossiper.getLiveMembers().forEach(this::reportCurrentSchemaVersionOnEndpoint); - } - - synchronized void removeAndIgnoreEndpoint(InetAddressAndPort endpoint) - { - logger.debug("Removing and ignoring endpoint {}", endpoint); - Preconditions.checkArgument(endpoint != null); - // TODO The endpoint address is now ignored but when a node with the same address is added again later, - // there will be no way to include it in schema synchronization other than restarting each other node - // see https://issues.apache.org/jira/browse/CASSANDRA-17883 for details - ignoredEndpoints.add(endpoint); - Set versions = ImmutableSet.copyOf(versionInfo.keySet()); - for (UUID version : versions) - { - removeEndpointFromVersion(endpoint, version); - } - } - - private Future scheduleSchemaPull(InetAddressAndPort endpoint, VersionInfo info) - { - FutureTask task = new FutureTask<>(() -> pullSchema(endpoint, new Callback(endpoint, info))); - - if (shouldPullImmediately(endpoint, info.version)) - { - logger.debug("Pulling {} immediately from {}", info, endpoint); - submitToMigrationIfNotShutdown(task); - } - else - { - logger.debug("Postponing pull of {} from {} for {}ms", info, endpoint, MIGRATION_DELAY_IN_MS); - ScheduledExecutors.nonPeriodicTasks.schedule(() -> submitToMigrationIfNotShutdown(task), MIGRATION_DELAY_IN_MS, TimeUnit.MILLISECONDS); - } - - return task; - } - - void announce(UUID schemaVersion) - { - if (gossiper.isEnabled()) - gossiper.addLocalApplicationState(ApplicationState.SCHEMA, StorageService.instance.valueFactory.schema(schemaVersion)); - SchemaDiagnostics.versionAnnounced(Schema.instance); - } - - private Future submitToMigrationIfNotShutdown(Runnable task) - { - boolean skipped = false; - try - { - if (executor.isShutdown() || executor.isTerminated()) - { - skipped = true; - return ImmediateFuture.success(null); - } - return executor.submit(task); - } - catch (RejectedExecutionException ex) - { - skipped = true; - return ImmediateFuture.success(null); - } - finally - { - if (skipped) - { - logger.info("Skipped scheduled pulling schema from other nodes: the MIGRATION executor service has been shutdown."); - } - } - } - - private class Callback implements RequestCallback> - { - final InetAddressAndPort endpoint; - final VersionInfo info; - - public Callback(InetAddressAndPort endpoint, VersionInfo info) - { - this.endpoint = endpoint; - this.info = info; - } - - public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) - { - fail(); - } - - Future fail() - { - return pullComplete(endpoint, info, false); - } - - public void onResponse(Message> message) - { - response(message.payload); - } - - Future response(Collection mutations) - { - synchronized (info) - { - if (shouldApplySchemaFor(info)) - { - try - { - schemaUpdateCallback.accept(endpoint, mutations); - } - catch (Exception e) - { - logger.error(String.format("Unable to merge schema from %s", endpoint), e); - return fail(); - } - } - return pullComplete(endpoint, info, true); - } - } - - public boolean isLatencyForSnitch() - { - return false; - } - } - - private void pullSchema(InetAddressAndPort endpoint, RequestCallback> callback) - { - if (!gossiper.isAlive(endpoint)) - { - noSpamLogger.warn("Can't send schema pull request: node {} is down.", endpoint); - callback.onFailure(endpoint, RequestFailureReason.UNKNOWN); - return; - } - - // There is a chance that quite some time could have passed between now and the MM#maybeScheduleSchemaPull(), - // potentially enough for the endpoint node to restart - which is an issue if it does restart upgraded, with - // a higher major. - if (!shouldPullFromEndpoint(endpoint)) - { - logger.info("Skipped sending a migration request: node {} has a higher major version now.", endpoint); - callback.onFailure(endpoint, RequestFailureReason.UNKNOWN); - return; - } - - logger.debug("Requesting schema from {}", endpoint); - sendMigrationMessage(endpoint, callback); - } - - private void sendMigrationMessage(InetAddressAndPort endpoint, RequestCallback> callback) - { - Message message = Message.out(Verb.SCHEMA_PULL_REQ, NoPayload.noPayload); - logger.info("Sending schema pull request to {}", endpoint); - messagingService.sendWithCallback(message, endpoint, callback); - } - - private synchronized Future pullComplete(InetAddressAndPort endpoint, VersionInfo info, boolean wasSuccessful) - { - if (wasSuccessful) - info.markReceived(); - - info.outstandingRequests.remove(endpoint); - info.requestQueue.add(endpoint); - return maybePullSchema(info); - } - - /** - * Wait until we've received schema responses for all versions we're aware of - * @param waitMillis - * @return true if response for all schemas were received, false if we timed out waiting - */ - boolean awaitSchemaRequests(long waitMillis) - { - if (!FBUtilities.getBroadcastAddressAndPort().equals(InetAddressAndPort.getLoopbackAddress())) - Gossiper.waitToSettle(); - - if (versionInfo.isEmpty()) - logger.debug("Nothing in versionInfo - so no schemas to wait for"); - - List signalList = null; - try - { - synchronized (this) - { - signalList = new ArrayList<>(versionInfo.size()); - for (VersionInfo version : versionInfo.values()) - { - if (version.wasReceived()) - continue; - - signalList.add(version.register()); - } - - if (signalList.isEmpty()) - return true; - } - - long deadline = nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitMillis); - return signalList.stream().allMatch(signal -> signal.awaitUntilUninterruptibly(deadline)); - } - finally - { - if (signalList != null) - signalList.forEach(WaitQueue.Signal::cancel); - } - } - - Pair, Set> pushSchemaMutations(Collection schemaMutations) - { - logger.debug("Pushing schema mutations: {}", schemaMutations); - Set schemaDestinationEndpoints = new HashSet<>(); - Set schemaEndpointsIgnored = new HashSet<>(); - Message> message = Message.out(SCHEMA_PUSH_REQ, schemaMutations); - for (InetAddressAndPort endpoint : gossiper.getLiveMembers()) - { - if (shouldPushSchemaTo(endpoint)) - { - logger.debug("Pushing schema mutations to {}: {}", endpoint, schemaMutations); - messagingService.send(message, endpoint); - schemaDestinationEndpoints.add(endpoint); - } - else - { - schemaEndpointsIgnored.add(endpoint); - } - } - - return Pair.create(schemaDestinationEndpoints, schemaEndpointsIgnored); - } - - private boolean shouldPushSchemaTo(InetAddressAndPort endpoint) - { - // only push schema to nodes with known and equal versions - return !endpoint.equals(FBUtilities.getBroadcastAddressAndPort()) - && messagingService.versions.knows(endpoint) - && messagingService.versions.getRaw(endpoint) == MessagingService.current_version; - } - -} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/schema/OfflineSchemaUpdateHandler.java b/src/java/org/apache/cassandra/schema/OfflineSchemaUpdateHandler.java deleted file mode 100644 index 16694b1f7c..0000000000 --- a/src/java/org/apache/cassandra/schema/OfflineSchemaUpdateHandler.java +++ /dev/null @@ -1,96 +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.schema; - -import java.time.Duration; -import java.util.UUID; -import java.util.function.BiConsumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; -import org.apache.cassandra.utils.ByteArrayUtil; -import org.apache.cassandra.utils.concurrent.Awaitable; -import org.apache.cassandra.utils.concurrent.ImmediateFuture; - -/** - * Update handler which works only in memory. It does not load or save the schema anywhere. It is used in client mode - * applications. - */ -public class OfflineSchemaUpdateHandler implements SchemaUpdateHandler -{ - private static final Logger logger = LoggerFactory.getLogger(OfflineSchemaUpdateHandler.class); - - private final BiConsumer updateCallback; - - private volatile DistributedSchema schema = DistributedSchema.EMPTY; - - public OfflineSchemaUpdateHandler(BiConsumer updateCallback) - { - this.updateCallback = updateCallback; - } - - @Override - public void start() - { - // no-op - } - - @Override - public boolean waitUntilReady(Duration timeout) - { - return true; - } - - @Override - public synchronized SchemaTransformationResult apply(SchemaTransformation transformation, boolean local) - { - DistributedSchema before = schema; - Keyspaces afterKeyspaces = transformation.apply(before.getKeyspaces()); - Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before.getKeyspaces(), afterKeyspaces); - - if (diff.isEmpty()) - return new SchemaTransformationResult(before, before, diff); - - DistributedSchema after = new DistributedSchema(afterKeyspaces, UUID.nameUUIDFromBytes(ByteArrayUtil.bytes(afterKeyspaces.hashCode()))); - SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff); - this.schema = after; - logger.debug("Schema updated: {}", update); - updateCallback.accept(update, true); - - return update; - } - - @Override - public void reset(boolean local) - { - if (!local) - throw new UnsupportedOperationException(); - - apply(ignored -> SchemaKeyspace.fetchNonSystemKeyspaces(), local); - } - - @Override - public synchronized Awaitable clear() - { - this.schema = DistributedSchema.EMPTY; - return ImmediateFuture.success(true); - } -} diff --git a/src/java/org/apache/cassandra/schema/PartitionDenylist.java b/src/java/org/apache/cassandra/schema/PartitionDenylist.java index 53fcb5e2b1..9c08bb1ce3 100644 --- a/src/java/org/apache/cassandra/schema/PartitionDenylist.java +++ b/src/java/org/apache/cassandra/schema/PartitionDenylist.java @@ -44,8 +44,8 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.RequestExecutionException; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.range.RangeCommands; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.NoSpamLogger; @@ -166,8 +166,15 @@ public class PartitionDenylist private boolean checkDenylistNodeAvailability() { - boolean sufficientNodes = RangeCommands.sufficientLiveNodesForSelectStar(SystemDistributedKeyspace.PartitionDenylistTable, - DatabaseDescriptor.getDenylistConsistencyLevel()); + TableMetadata denyListTable = ClusterMetadata.current().schema.getKeyspaceMetadata(SystemDistributedKeyspace.NAME) + .getTableOrViewNullable(SystemDistributedKeyspace.PARTITION_DENYLIST_TABLE); + if (denyListTable == null) + { + logger.warn("Partition denylist table metadata not found"); + return false; + } + + boolean sufficientNodes = RangeCommands.sufficientLiveNodesForSelectStar(denyListTable, DatabaseDescriptor.getDenylistConsistencyLevel()); if (!sufficientNodes) { AVAILABILITY_LOGGER.warn("Attempting to load denylist and not enough nodes are available for a {} refresh. Reload the denylist when unavailable nodes are recovered to ensure your denylist remains in sync.", @@ -433,13 +440,13 @@ public class PartitionDenylist final Set keys = new HashSet<>(); final NavigableSet tokens = new TreeSet<>(); - + ClusterMetadata metadata = ClusterMetadata.current(); int processed = 0; for (final UntypedResultSet.Row row : results) { final ByteBuffer key = row.getBlob("key"); keys.add(key); - tokens.add(StorageService.instance.getTokenMetadata().partitioner.getToken(key)); + tokens.add(metadata.tokenMap.partitioner().getToken(key)); processed++; if (processed >= limit) diff --git a/src/java/org/apache/cassandra/schema/ReplicationParams.java b/src/java/org/apache/cassandra/schema/ReplicationParams.java index 2998aa57ad..cf445a4b78 100644 --- a/src/java/org/apache/cassandra/schema/ReplicationParams.java +++ b/src/java/org/apache/cassandra/schema/ReplicationParams.java @@ -17,21 +17,41 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CqlBuilder; -import org.apache.cassandra.locator.*; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.LocalStrategy; +import org.apache.cassandra.locator.MetaStrategy; +import org.apache.cassandra.locator.NetworkTopologyStrategy; +import org.apache.cassandra.locator.SimpleStrategy; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.TypeSizes.sizeof; public final class ReplicationParams { + public static final Serializer serializer = new Serializer(); + public static final MessageSerializer messageSerializer = new MessageSerializer(); + public static final String CLASS = "class"; public final Class klass; @@ -43,12 +63,55 @@ public final class ReplicationParams this.options = ImmutableMap.copyOf(options); } - static ReplicationParams local() + public static ReplicationParams local() { return new ReplicationParams(LocalStrategy.class, ImmutableMap.of()); } - static ReplicationParams simple(int replicationFactor) + public boolean isLocal() + { + return klass == LocalStrategy.class; + } + + public boolean isMeta() + { + return klass == MetaStrategy.class; + } + + /** + * For backward-compatibility reasons we are persisting replication params for cluster metadata as non-meta + * replication params. This means that when we are creating mutations, meta params will be written to as Network + * Topology Strategy (in local DC, if no DCs are specified), and when schema is loaded from system tables, we will + * create a meta strategy instance for cluster metadata keyspace. + */ + public ReplicationParams asMeta() + { + assert !isMeta() : this; + if (options.containsKey(SimpleStrategy.REPLICATION_FACTOR)) + { + Map dcRf = new HashMap<>(); + String rf = options.get(SimpleStrategy.REPLICATION_FACTOR); + dcRf.put(DatabaseDescriptor.getLocalDataCenter(), rf); + return new ReplicationParams(MetaStrategy.class, dcRf); + } + + return new ReplicationParams(MetaStrategy.class, options); + } + + /** + * Counterpart of `asMeta`, see comment for asMeta for details. + */ + public ReplicationParams asNonMeta() + { + assert isMeta() : this; + if (options.containsKey(SimpleStrategy.REPLICATION_FACTOR)) + return new ReplicationParams(SimpleStrategy.class, options); + + return new ReplicationParams(NetworkTopologyStrategy.class, options); + } + + @VisibleForTesting + public static ReplicationParams simple(int replicationFactor) { return new ReplicationParams(SimpleStrategy.class, ImmutableMap.of("replication_factor", Integer.toString(replicationFactor))); } @@ -58,6 +121,44 @@ public final class ReplicationParams return new ReplicationParams(SimpleStrategy.class, ImmutableMap.of("replication_factor", replicationFactor)); } + public static ReplicationParams simpleMeta(int replicationFactor, Set knownDatacenters) + { + if (replicationFactor <= 0) + throw new IllegalStateException("Replication factor should be strictly positive"); + if (knownDatacenters.isEmpty()) + throw new IllegalStateException("No known datacenters"); + String dc = knownDatacenters.stream().min(Comparator.comparing(s -> s)).get(); + Map dcRf = new HashMap<>(); + dcRf.put(dc, replicationFactor); + return ntsMeta(dcRf); + } + + public static ReplicationParams ntsMeta(Map replicationFactor) + { + Map rfAsString = new HashMap<>(); + int aggregate = 0; + for (Map.Entry e : replicationFactor.entrySet()) + { + int rf = e.getValue(); + aggregate += rf; + if (rf <= 0) + throw new IllegalStateException("Replication factor should be strictly positive: " + rf); + rfAsString.put(e.getKey(), Integer.toString(rf)); + } + + if (aggregate <= 0) + throw new IllegalArgumentException("Aggregate replication factor should be strictly positive: " + replicationFactor); + return new ReplicationParams(MetaStrategy.class, rfAsString); + } + + // meta replication, i.e. the replication strategy used for topology decisions + public static ReplicationParams meta(ClusterMetadata metadata) + { + ReplicationParams metaParams = metadata.schema.getKeyspaceMetadata(SchemaConstants.METADATA_KEYSPACE_NAME).params.replication; + assert metaParams.isMeta() : metaParams; + return metaParams; + } + static ReplicationParams nts(Object... args) { assert args.length % 2 == 0; @@ -71,12 +172,10 @@ public final class ReplicationParams return new ReplicationParams(NetworkTopologyStrategy.class, options); } - public void validate(String name, ClientState state) + public void validate(String name, ClientState state, ClusterMetadata metadata) { // Attempt to instantiate the ARS, which will throw a ConfigurationException if the options aren't valid. - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - IEndpointSnitch eps = DatabaseDescriptor.getEndpointSnitch(); - AbstractReplicationStrategy.validateReplicationStrategy(name, klass, tmd, eps, options, state); + AbstractReplicationStrategy.validateReplicationStrategy(name, klass, metadata, options, state); } public static ReplicationParams fromMap(Map map) { @@ -147,4 +246,76 @@ public final class ReplicationParams builder.append('}'); } + + public static class Serializer implements MetadataSerializer + { + public void serialize(ReplicationParams t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.klass.getCanonicalName()); + out.writeUnsignedVInt32(t.options.size()); + for (Map.Entry option : t.options.entrySet()) + { + out.writeUTF(option.getKey()); + out.writeUTF(option.getValue()); + } + } + + public ReplicationParams deserialize(DataInputPlus in, Version version) throws IOException + { + String klassName = in.readUTF(); + int size = in.readUnsignedVInt32(); + Map options = new HashMap<>(size); + for (int i = 0; i < size; i++) + options.put(in.readUTF(), in.readUTF()); + return new ReplicationParams(FBUtilities.classForName(klassName, "ReplicationStrategy"), options); + } + + public long serializedSize(ReplicationParams t, Version version) + { + long size = sizeof(t.klass.getCanonicalName()); + size += TypeSizes.sizeofUnsignedVInt(t.options.size()); + for (Map.Entry option : t.options.entrySet()) + { + size += sizeof(option.getKey()); + size += sizeof(option.getValue()); + } + return size; + } + } + + public static class MessageSerializer implements IVersionedSerializer + { + public void serialize(ReplicationParams t, DataOutputPlus out, int version) throws IOException + { + out.writeUTF(t.klass.getCanonicalName()); + out.writeUnsignedVInt32(t.options.size()); + for (Map.Entry option : t.options.entrySet()) + { + out.writeUTF(option.getKey()); + out.writeUTF(option.getValue()); + } + } + + public ReplicationParams deserialize(DataInputPlus in, int version) throws IOException + { + String klassName = in.readUTF(); + int size = in.readUnsignedVInt32(); + Map options = new HashMap<>(size); + for (int i=0; i option : t.options.entrySet()) + { + size += sizeof(option.getKey()); + size += sizeof(option.getValue()); + } + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/Schema.java b/src/java/org/apache/cassandra/schema/Schema.java index b704fd2997..147d24a804 100644 --- a/src/java/org/apache/cassandra/schema/Schema.java +++ b/src/java/org/apache/cassandra/schema/Schema.java @@ -17,55 +17,35 @@ */ package org.apache.cassandra.schema; -import java.time.Duration; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; import java.util.function.Supplier; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.collect.MapDifference; -import com.google.common.collect.Sets; -import com.google.common.collect.Streams; +import com.google.common.collect.ImmutableSet; import org.apache.commons.lang3.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.cql3.functions.Function; -import org.apache.cassandra.cql3.functions.FunctionName; -import org.apache.cassandra.cql3.functions.UserFunction; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.KeyspaceNotDefinedException; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; +import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.io.sstable.Descriptor; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.LocalStrategy; -import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; -import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; -import org.apache.cassandra.service.PendingRangeCalculatorService; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.concurrent.Awaitable; -import org.apache.cassandra.utils.concurrent.LoadingMap; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.transformations.AlterSchema; import static com.google.common.collect.Iterables.size; -import static java.lang.String.format; import static org.apache.cassandra.config.DatabaseDescriptor.isDaemonInitialized; import static org.apache.cassandra.config.DatabaseDescriptor.isToolInitialized; @@ -76,234 +56,134 @@ import static org.apache.cassandra.config.DatabaseDescriptor.isToolInitialized; * This class should be the only entity used to query and manage schema. Internal details should not be access in * production code (would be great if they were not accessed in the test code as well). *

    - * TL;DR: All modifications are made using the implementation of {@link SchemaUpdateHandler} obtained from the provided + * TL;DR: All modifications are made using the implementation of SchemaUpdateHandle obtained from the provided * factory. After each modification, the internally managed table metadata refs and keyspaces instances are updated and * notifications are sent to the registered listeners. * When the schema change is applied by the update handler (regardless it is initiated locally or received from outside), * the registered callback is executed which performs the remaining updates for tables metadata refs and keyspace - * instances (see {@link #mergeAndUpdateVersion(SchemaTransformationResult, boolean)}). + * instances (see mergeAndUpdateVersion(SchemaTransformationResult). */ -public class Schema implements SchemaProvider +public final class Schema implements SchemaProvider { private static final Logger logger = LoggerFactory.getLogger(Schema.class); - public static final Schema instance = new Schema(); + private static final boolean FORCE_LOAD_LOCAL_KEYSPACES = CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.getBoolean(); - private volatile Keyspaces distributedKeyspaces = Keyspaces.none(); + public static SchemaProvider instance = initialize(); + + private static Schema initialize() + { + Keyspaces initialLocal = ((FORCE_LOAD_LOCAL_KEYSPACES || isDaemonInitialized() || isToolInitialized())) + ? Keyspaces.of(SchemaKeyspace.metadata(), + SystemKeyspace.metadata()) + : Keyspaces.NONE; + Schema schema = new Schema(initialLocal); + for (KeyspaceMetadata ks : schema.localKeyspaces) + schema.localKeyspaceInstances.put(ks.name, new LazyVariable<>(() -> Keyspace.forSchema(ks.name, schema))); + return schema; + } private final Keyspaces localKeyspaces; - - private volatile TableMetadataRefCache tableMetadataRefCache = TableMetadataRefCache.EMPTY; - - // Keyspace objects, one per keyspace. Only one instance should ever exist for any given keyspace. - // We operate on loading map because we need to achieve atomic initialization with at-most-once semantics for - // loadFunction. Although it seems that this is a valid case for using ConcurrentHashMap.computeIfAbsent, - // we should not use it because we have no knowledge about the loadFunction and in fact that load function may - // do some nested calls to maybeAddKeyspaceInstance, also using different threads, and in a blocking manner. - // This may lead to a deadlock. The documentation of ConcurrentHashMap says that manipulating other keys inside - // the lambda passed to the computeIfAbsent method is prohibited. - private final LoadingMap keyspaceInstances = new LoadingMap<>(); - - private volatile UUID version = SchemaConstants.emptyVersion; - private final SchemaChangeNotifier schemaChangeNotifier = new SchemaChangeNotifier(); - - public final SchemaUpdateHandler updateHandler; - - private final boolean online; + private final Map> localKeyspaceInstances = new HashMap<>(); /** * Initialize empty schema object and load the hardcoded system tables */ - private Schema() + private Schema(Keyspaces initialLocalKeyspaces) { - this.online = isDaemonInitialized(); - this.localKeyspaces = (CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.getBoolean() || isDaemonInitialized() || isToolInitialized()) - ? Keyspaces.of(SchemaKeyspace.metadata(), SystemKeyspace.metadata()) - : Keyspaces.none(); - - this.localKeyspaces.forEach(this::loadNew); - this.updateHandler = SchemaUpdateHandlerFactoryProvider.instance.get().getSchemaUpdateHandler(online, this::mergeAndUpdateVersion); - } - - @VisibleForTesting - public Schema(boolean online, Keyspaces localKeyspaces, SchemaUpdateHandler updateHandler) - { - this.online = online; - this.localKeyspaces = localKeyspaces; - this.updateHandler = updateHandler; - } - - public void startSync() - { - logger.debug("Starting update handler"); - updateHandler.start(); - } - - public boolean waitUntilReady(Duration timeout) - { - logger.debug("Waiting for update handler to be ready..."); - return updateHandler.waitUntilReady(timeout); + localKeyspaces = initialLocalKeyspaces; } /** - * Load keyspaces definitions from local storage, see {@link SchemaUpdateHandler#reset(boolean)}. - */ - public void loadFromDisk() - { - SchemaDiagnostics.schemaLoading(this); - updateHandler.reset(true); - SchemaDiagnostics.schemaLoaded(this); - } - - /** - * Update (or insert) new keyspace definition + * Add entries to system_schema.* for the hardcoded system keyspaces * - * @param ksm The metadata about keyspace + * See CASSANDRA-16856/16996. Make sure schema pulls are synchronized to prevent concurrent schema pull/writes */ - private synchronized void load(KeyspaceMetadata ksm) + public synchronized void saveSystemKeyspace() { - Preconditions.checkArgument(!SchemaConstants.isLocalSystemKeyspace(ksm.name)); - KeyspaceMetadata previous = distributedKeyspaces.getNullable(ksm.name); - - if (previous == null) - loadNew(ksm); - else - reload(previous, ksm); - - distributedKeyspaces = distributedKeyspaces.withAddedOrUpdated(ksm); - } - - private synchronized void loadNew(KeyspaceMetadata ksm) - { - this.tableMetadataRefCache = tableMetadataRefCache.withNewRefs(ksm); - - SchemaDiagnostics.metadataInitialized(this, ksm); - } - - private synchronized void reload(KeyspaceMetadata previous, KeyspaceMetadata updated) - { - Keyspace keyspace = getKeyspaceInstance(updated.name); - if (null != keyspace) - keyspace.setMetadata(updated); - - Tables.TablesDiff tablesDiff = Tables.diff(previous.tables, updated.tables); - Views.ViewsDiff viewsDiff = Views.diff(previous.views, updated.views); - - MapDifference indexesDiff = previous.tables.indexesDiff(updated.tables); - - this.tableMetadataRefCache = tableMetadataRefCache.withUpdatedRefs(previous, updated); - - SchemaDiagnostics.metadataReloaded(this, previous, updated, tablesDiff, viewsDiff, indexesDiff); + SchemaKeyspace.saveSystemKeyspacesSchema(); } + @Override public void registerListener(SchemaChangeListener listener) { schemaChangeNotifier.registerListener(listener); } - @SuppressWarnings("unused") + @Override public void unregisterListener(SchemaChangeListener listener) { schemaChangeNotifier.unregisterListener(listener); } + @Override + public SchemaChangeNotifier schemaChangeNotifier() + { + return schemaChangeNotifier; + } + /** * Get keyspace instance by name * * @param keyspaceName The name of the keyspace - * - * @return Keyspace object or null if keyspace was not found + * @return Keyspace object or null if keyspace was not found, or if the keyspace has not completed construction yet */ @Override public Keyspace getKeyspaceInstance(String keyspaceName) { - return keyspaceInstances.getIfReady(keyspaceName); + if (SchemaConstants.isVirtualSystemKeyspace(keyspaceName)) + return null; + else if (SchemaConstants.isLocalSystemKeyspace(keyspaceName)) + return localKeyspaceInstances.get(keyspaceName).get(); + else + return ClusterMetadata.current().schema.getKeyspace(keyspaceName); } - /** - * Returns {@link ColumnFamilyStore} by the table identifier. Note that though, if called for {@link TableMetadata#id}, - * when metadata points to a secondary index table, the {@link TableMetadata#id} denotes the identifier of the main - * table, not the index table. Thus, this method will return CFS of the main table rather than, probably expected, - * CFS for the index backing table. - */ - public ColumnFamilyStore getColumnFamilyStoreInstance(TableId id) + public Keyspaces distributedAndLocalKeyspaces() { - TableMetadata metadata = getTableMetadata(id); - if (metadata == null) - return null; - - Keyspace instance = getKeyspaceInstance(metadata.keyspace); - if (instance == null) - return null; - - return instance.hasColumnFamilyStore(metadata.id) - ? instance.getColumnFamilyStore(metadata.id) - : null; + // TODO reimplement perf fix from CASSANDRA-18921 + return Keyspaces.NONE.with(localKeyspaces).with(distributedKeyspaces()); } @Override - public Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction) - { - return keyspaceInstances.blockingLoadIfAbsent(keyspaceName, loadFunction); - } - - private Keyspace maybeRemoveKeyspaceInstance(String keyspaceName, Consumer unloadFunction) - { - try - { - return keyspaceInstances.blockingUnloadIfPresent(keyspaceName, unloadFunction); - } - catch (LoadingMap.UnloadExecutionException e) - { - throw new AssertionError("Failed to unload the keyspace " + keyspaceName, e); - } - } - public Keyspaces distributedKeyspaces() { - return distributedKeyspaces; + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + return Keyspaces.NONE; + + return metadata.schema.getKeyspaces(); } - /** - * Compute the largest gc grace seconds amongst all the tables - * @return the largest gcgs. - */ - public int largestGcgs() + public Keyspaces localKeyspaces() { - return Streams.concat(distributedKeyspaces.stream(), localKeyspaces.stream()) - .flatMap(ksm -> ksm.tables.stream()) - .mapToInt(tm -> tm.params.gcGraceSeconds) - .max() - .orElse(Integer.MIN_VALUE); - } - - /** - * Remove keyspace definition from system - * - * @param ksm The keyspace definition to remove - */ - private synchronized void unload(KeyspaceMetadata ksm) - { - distributedKeyspaces = distributedKeyspaces.without(ksm.name); - - this.tableMetadataRefCache = tableMetadataRefCache.withRemovedRefs(ksm); - - SchemaDiagnostics.metadataRemoved(this, ksm); + return localKeyspaces; } public int getNumberOfTables() { - return Streams.concat(distributedKeyspaces.stream(), localKeyspaces.stream()) - .mapToInt(k -> size(k.tablesAndViews())) - .sum(); + return distributedAndLocalKeyspaces().stream() + .mapToInt(k -> size(k.tablesAndViews())) + .sum(); + } + + public Optional getIndexMetadata(String keyspace, String index) + { + assert keyspace != null; + assert index != null; + + KeyspaceMetadata ksm = getKeyspaceMetadata(keyspace); + if (ksm == null) + return Optional.empty(); + + return ksm.getIndexMetadata(index); } public ViewMetadata getView(String keyspaceName, String viewName) { assert keyspaceName != null; - KeyspaceMetadata ksm = distributedKeyspaces.getNullable(keyspaceName); - ksm = ksm != null ? ksm : localKeyspaces.getNullable(keyspaceName); + KeyspaceMetadata ksm = distributedAndLocalKeyspaces().getNullable(keyspaceName); return (ksm == null) ? null : ksm.views.getNullable(viewName); } @@ -318,18 +198,30 @@ public class Schema implements SchemaProvider public KeyspaceMetadata getKeyspaceMetadata(String keyspaceName) { assert keyspaceName != null; - KeyspaceMetadata ksm = distributedKeyspaces.getNullable(keyspaceName); - ksm = ksm != null ? ksm : localKeyspaces.getNullable(keyspaceName); - return null != ksm ? ksm : VirtualKeyspaceRegistry.instance.getKeyspaceMetadataNullable(keyspaceName); + KeyspaceMetadata keyspace; + if (SchemaConstants.isLocalSystemKeyspace(keyspaceName)) + keyspace = localKeyspaces.getNullable(keyspaceName); + else + keyspace = distributedKeyspaces().getNullable(keyspaceName); + + return null != keyspace ? keyspace : VirtualKeyspaceRegistry.instance.getKeyspaceMetadataNullable(keyspaceName); + } + + /** + * Returns all non-local keyspaces whose replication strategy is not {@link LocalStrategy}. + */ + public Keyspaces getNonLocalStrategyKeyspaces() + { + return distributedKeyspaces().filter(keyspace -> keyspace.params.replication.klass != LocalStrategy.class); } /** * Returns user keyspaces, that is all but {@link SchemaConstants#LOCAL_SYSTEM_KEYSPACE_NAMES}, * {@link SchemaConstants#REPLICATED_SYSTEM_KEYSPACE_NAMES} or virtual keyspaces. */ - public Sets.SetView getUserKeyspaces() + public Keyspaces getUserKeyspaces() { - return Sets.difference(distributedKeyspaces.names(), SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES); + return distributedKeyspaces().without(SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES); } /** @@ -341,7 +233,7 @@ public class Schema implements SchemaProvider public Iterable getTablesAndViews(String keyspaceName) { Preconditions.checkNotNull(keyspaceName); - KeyspaceMetadata ksm = ObjectUtils.getFirstNonNull(() -> distributedKeyspaces.getNullable(keyspaceName), + KeyspaceMetadata ksm = ObjectUtils.getFirstNonNull(() -> distributedKeyspaces().getNullable(keyspaceName), () -> localKeyspaces.getNullable(keyspaceName)); Preconditions.checkNotNull(ksm, "Keyspace %s not found", keyspaceName); return ksm.tablesAndViews(); @@ -350,52 +242,9 @@ public class Schema implements SchemaProvider /** * @return a set of local and distributed keyspace names; it does not include virtual keyspaces */ - public Sets.SetView getKeyspaces() + public ImmutableSet getKeyspaces() { - return Sets.union(distributedKeyspaces.names(), localKeyspaces.names()); - } - - public Keyspaces getLocalKeyspaces() - { - return localKeyspaces; - } - - /* TableMetadata/Ref query/control methods */ - - /** - * Given a keyspace name and table/view name, get the table metadata - * reference. If the keyspace name or table/view name is not present - * this method returns null. - * - * @return TableMetadataRef object or null if it wasn't found - */ - @Override - public TableMetadataRef getTableMetadataRef(String keyspace, String table) - { - return tableMetadataRefCache.getTableMetadataRef(keyspace, table); - } - - public TableMetadataRef getIndexTableMetadataRef(String keyspace, String index) - { - return tableMetadataRefCache.getIndexTableMetadataRef(keyspace, index); - } - - /** - * Get Table metadata by its identifier - * - * @param id table or view identifier - * @return metadata about Table or View - */ - @Override - public TableMetadataRef getTableMetadataRef(TableId id) - { - return tableMetadataRefCache.getTableMetadataRef(id); - } - - @Override - public TableMetadataRef getTableMetadataRef(Descriptor descriptor) - { - return getTableMetadataRef(descriptor.ksname, descriptor.cfname); + return ImmutableSet.copyOf(distributedAndLocalKeyspaces().names()); } /** @@ -421,25 +270,10 @@ public class Schema implements SchemaProvider @Override public TableMetadata getTableMetadata(TableId id) { - return ObjectUtils.getFirstNonNull(() -> distributedKeyspaces.getTableOrViewNullable(id), - () -> localKeyspaces.getTableOrViewNullable(id), + return ObjectUtils.getFirstNonNull(() -> localKeyspaces.getTableOrViewNullable(id), + () -> distributedKeyspaces().getTableOrViewNullable(id), () -> VirtualKeyspaceRegistry.instance.getTableMetadataNullable(id)); - } - public TableMetadata validateTable(String keyspaceName, String tableName) - { - if (tableName.isEmpty()) - throw new InvalidRequestException("non-empty table is required"); - - KeyspaceMetadata keyspace = getKeyspaceMetadata(keyspaceName); - if (keyspace == null) - throw new KeyspaceNotDefinedException(format("keyspace %s does not exist", keyspaceName)); - - TableMetadata metadata = keyspace.getTableOrViewNullable(tableName); - if (metadata == null) - throw new InvalidRequestException(format("table %s does not exist", tableName)); - - return metadata; } public TableMetadata getTableMetadata(Descriptor descriptor) @@ -447,342 +281,118 @@ public class Schema implements SchemaProvider return getTableMetadata(descriptor.ksname, descriptor.cfname); } - /* Function helpers */ - - /** - * Get all user-defined function overloads with the specified name. - * - * @param name fully qualified function name - * @return an empty list if the keyspace or the function name are not found; - * a non-empty collection of {@link UserFunction} otherwise - */ - public Collection getUserFunctions(FunctionName name) + @Override + public ClusterMetadata submit(SchemaTransformation transformation) { - if (!name.hasKeyspace()) - throw new IllegalArgumentException(String.format("Function name must be fully qualified: got %s", name)); + logger.debug("Submitting schema transformation {}", transformation); - KeyspaceMetadata ksm = getKeyspaceMetadata(name.keyspace); - return ksm == null - ? Collections.emptyList() - : ksm.userFunctions.get(name); + // result of this execution can be either a complete failure/timeout, or a success, but together with a log of + // operations that have to be applied before we can do anything + return ClusterMetadataService.instance() + .commit(new AlterSchema(transformation, this), + (metadata) -> metadata, + (code, reason) -> { + switch (code) + { + case ALREADY_EXISTS: + throw new AlreadyExistsException(reason); + case CONFIG_ERROR: + throw new ConfigurationException(reason); + case SYNTAX_ERROR: + throw new SyntaxException(reason); + case UNAUTHORIZED: + throw new UnauthorizedException(reason); + default: + throw new InvalidRequestException(reason); + } + }); } - /** - * Find the function with the specified name and arguments. - * - * @param name fully qualified function name - * @param argTypes function argument types - * @return an empty {@link Optional} if the keyspace or the function name are not found; - * a non-empty optional of {@link Function} otherwise - */ - public Optional findUserFunction(FunctionName name, List> argTypes) + // We need to lazy-initialize schema for test purposes: since column families are initialized + // eagerly, if local schema initialization is attempted before commit log instance is started, + // cf initialization will fail to grab a current commit log position. + // + // This could be further improved by providing a special Schema instance for tests, or + // using supplier with precomputed value for regular code path, and lazy variable for tests. + // + // TODO: it _might_ be useful to avoid eagerly initializing distributed keyspaces, especially in + // when the number of tables is large. + private final static class LazyVariable implements Supplier { - if (!name.hasKeyspace()) - throw new IllegalArgumentException(String.format("Function name must be fully quallified: got %s", name)); + private final AtomicReference ref; + private final Supplier run; - return Optional.ofNullable(getKeyspaceMetadata(name.keyspace)) - .flatMap(ksm -> ksm.userFunctions.find(name, argTypes)); - } - - /* Version control */ - - /** - * Returns the current schema version. Although, if the schema is being updated while the method was called, it - * can return a stale version which does not correspond to the current keyspaces metadata. It is because the schema - * version is unknown for the partially applied changes and is updated after the entire schema change is completed. - *

    - * This method should be used only internally by {@link Schema} or {@link SchemaUpdateHandler} implementations. - * Please use {@link #getDistributedSchemaBlocking()} to get schema version consistently in other cases. - */ - public UUID getVersion() - { - return version; - } - - /** - * Returns the current keyspaces metadata and version synchronouly. If the schema is in the middle of a multistep - * transformation, the method blocks until the update is completed. - */ - public synchronized DistributedSchema getDistributedSchemaBlocking() - { - return new DistributedSchema(distributedKeyspaces, version); - } - - /** - * Checks whether the given schema version is the same as the current local schema. - * Note that this method is non-blocking and may use a stale schema version for comparison - see {@link #getVersion()}. - */ - public boolean isSameVersion(UUID schemaVersion) - { - return schemaVersion != null && schemaVersion.equals(version); - } - - /** - * Checks whether the current schema is empty. - * Note that this method is non-blocking and may use a stale schema version for comparison - see {@link #getVersion()}. - */ - public boolean isEmpty() - { - return SchemaConstants.emptyVersion.equals(version); - } - - /** - * Read schema from system keyspace and calculate MD5 digest of every row, resulting digest - * will be converted into UUID which would act as content-based version of the schema. - * - * See CASSANDRA-16856/16996. Make sure schema pulls are synchronized to prevent concurrent schema pull/writes - */ - private synchronized void updateVersion(UUID version) - { - this.version = version; - SchemaDiagnostics.versionUpdated(this); - } - - /** - * When we receive {@link SchemaTransformationResult} in a callback invocation, the transformation result includes - * pre-transformation and post-transformation schema metadata and versions, and a diff between them. Basically - * we expect that the local image of the schema metadata ({@link #distributedKeyspaces}) and version ({@link #version}) - * are the same as pre-transformation. However, it might not always be true because some changes might not be - * applied completely due to some errors. This methods is to emit warning in such case and recalculate diff so that - * it contains the changes between the local schema image ({@link #distributedKeyspaces} and the post-transformation - * schema. That recalculation allows the following updates in the callback to recover the schema. - * - * @param result the incoming transformation result - * @return recalculated transformation result if needed, otherwise the provided incoming result - */ - private synchronized SchemaTransformationResult localDiff(SchemaTransformationResult result) - { - Keyspaces localBefore = distributedKeyspaces; - UUID localVersion = version; - boolean needNewDiff = false; - - if (!Objects.equals(localBefore, result.before.getKeyspaces())) + private LazyVariable(Supplier run) { - logger.info("Schema was different to what we expected: {}", Keyspaces.diff(result.before.getKeyspaces(), localBefore)); - needNewDiff = true; + this.ref = new AtomicReference<>(null); + this.run = run; } - if (!Objects.equals(localVersion, result.before.getVersion())) + public T get() { - logger.info("Schema version was different to what we expected: {} != {}", result.before.getVersion(), localVersion); - needNewDiff = true; - } - - if (needNewDiff) - return new SchemaTransformationResult(new DistributedSchema(localBefore, localVersion), - result.after, - Keyspaces.diff(localBefore, result.after.getKeyspaces())); - - return result; - } - - /* - * Reload schema from local disk. Useful if a user made changes to schema tables by hand, or has suspicion that - * in-memory representation got out of sync somehow with what's on disk. - */ - public void reloadSchemaAndAnnounceVersion() - { - updateHandler.reset(true); - } - - /** - * Merge remote schema in form of mutations with local and mutate ks/cf metadata objects - * (which also involves fs operations on add/drop ks/cf) - * - * @throws ConfigurationException If one of metadata attributes has invalid value - */ - @VisibleForTesting - public synchronized void mergeAndUpdateVersion(SchemaTransformationResult result, boolean dropData) - { - result = localDiff(result); - assert result.after.getKeyspaces().stream().noneMatch(ksm -> ksm.params.replication.klass == LocalStrategy.class) : "LocalStrategy should not be used"; - schemaChangeNotifier.notifyPreChanges(result); - merge(result.diff, dropData); - updateVersion(result.after.getVersion()); - if (online) - SystemKeyspace.updateSchemaVersion(result.after.getVersion()); - } - - public SchemaTransformationResult transform(SchemaTransformation transformation) - { - return transform(transformation, false); - } - - public SchemaTransformationResult transform(SchemaTransformation transformation, boolean local) - { - return updateHandler.apply(transformation, local); - } - - /** - * Clear all locally stored schema information and fetch schema from another node. - * Called by user (via JMX) who wants to get rid of schema disagreement. - */ - public void resetLocalSchema() - { - logger.debug("Clearing local schema..."); - - if (Gossiper.instance.getLiveMembers().stream().allMatch(ep -> FBUtilities.getBroadcastAddressAndPort().equals(ep))) - throw new InvalidRequestException("Cannot reset local schema when there are no other live nodes"); - - Awaitable clearCompletion = updateHandler.clear(); - try - { - if (!clearCompletion.await(StorageService.SCHEMA_DELAY_MILLIS, TimeUnit.MILLISECONDS)) + Object v = ref.get(); + if (v == null) { - throw new RuntimeException("Schema reset failed - no schema received from other nodes"); + Sentinel sentinel = new Sentinel(); + + if (ref.compareAndSet(null, sentinel)) + { + try + { + v = run.get(); + } + catch (Throwable t) + { + ref.compareAndSet(sentinel, null); + throw t; + } + boolean result = ref.compareAndSet(sentinel, v); + assert result; + } + else + { + return get(); + } } - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - throw new RuntimeException("Failed to reset schema - the thread has been interrupted"); - } - SchemaDiagnostics.schemaCleared(this); - logger.info("Local schema reset completed"); - } - - private void merge(KeyspacesDiff diff, boolean removeData) - { - diff.dropped.forEach(keyspace -> dropKeyspace(keyspace, removeData)); - diff.created.forEach(this::createKeyspace); - diff.altered.forEach(delta -> alterKeyspace(delta, removeData)); - } - - private void alterKeyspace(KeyspaceDiff delta, boolean dropData) - { - SchemaDiagnostics.keyspaceAltering(this, delta); - - boolean initialized = Keyspace.isInitialized(); - - Keyspace keyspace = initialized ? getKeyspaceInstance(delta.before.name) : null; - if (initialized) - { - assert keyspace != null; - assert delta.before.name.equals(delta.after.name); - - // drop tables and views - delta.views.dropped.forEach(v -> dropView(keyspace, v, dropData)); - delta.tables.dropped.forEach(t -> dropTable(keyspace, t, dropData)); - } - - load(delta.after); - - if (initialized) - { - // add tables and views - delta.tables.created.forEach(t -> createTable(keyspace, t)); - delta.views.created.forEach(v -> createView(keyspace, v)); - - // update tables and views - delta.tables.altered.forEach(diff -> alterTable(keyspace, diff.after)); - delta.views.altered.forEach(diff -> alterView(keyspace, diff.after)); - - // deal with all added, and altered views - Keyspace.open(delta.after.name, this, true).viewManager.reload(true); - } - - schemaChangeNotifier.notifyKeyspaceAltered(delta, dropData); - SchemaDiagnostics.keyspaceAltered(this, delta); - } - - private void createKeyspace(KeyspaceMetadata keyspace) - { - SchemaDiagnostics.keyspaceCreating(this, keyspace); - load(keyspace); - if (Keyspace.isInitialized()) - { - Keyspace.open(keyspace.name, this, true); - } - - schemaChangeNotifier.notifyKeyspaceCreated(keyspace); - SchemaDiagnostics.keyspaceCreated(this, keyspace); - - // If keyspace has been added, we need to recalculate pending ranges to make sure - // we send mutations to the correct set of bootstrapping nodes. Refer CASSANDRA-15433. - if (keyspace.params.replication.klass != LocalStrategy.class && Keyspace.isInitialized()) - { - PendingRangeCalculatorService.calculatePendingRanges(Keyspace.open(keyspace.name, this, true).getReplicationStrategy(), keyspace.name); + else + { + while (v instanceof Sentinel) + { + if (((Sentinel) v).thread == Thread.currentThread()) + { + throw new RuntimeException("Looks like we have a deadlock. Check sentinel for the original call.", + ((Sentinel) v).throwable); + } + v = ref.get(); + LockSupport.parkNanos(100); + } + } + return (T) v; } } - private void dropKeyspace(KeyspaceMetadata keyspaceMetadata, boolean dropData) + private static final class Sentinel { - SchemaDiagnostics.keyspaceDropping(this, keyspaceMetadata); + private final Thread thread; + private final Throwable throwable; - boolean initialized = Keyspace.isInitialized(); - Keyspace keyspace = initialized ? Keyspace.open(keyspaceMetadata.name, this, false) : null; - if (initialized) + private Sentinel() { - if (keyspace == null) - return; + this(Thread.currentThread(), new RuntimeException("Sentinel call") { + private final StackTraceElement[] trace = Thread.currentThread().getStackTrace(); - keyspaceMetadata.views.forEach(v -> dropView(keyspace, v, dropData)); - keyspaceMetadata.tables.forEach(t -> dropTable(keyspace, t, dropData)); - - // remove the keyspace from the static instances - Keyspace unloadedKeyspace = maybeRemoveKeyspaceInstance(keyspaceMetadata.name, ks -> { - ks.unload(dropData); - unload(keyspaceMetadata); + @Override + public StackTraceElement[] getStackTrace() + { + return trace; + } }); - assert unloadedKeyspace == keyspace; - - Keyspace.writeOrder.awaitNewBarrier(); } - else + + private Sentinel(Thread thread, Throwable throwable) { - unload(keyspaceMetadata); + this.thread = thread; + this.throwable = throwable; } - - schemaChangeNotifier.notifyKeyspaceDropped(keyspaceMetadata, dropData); - SchemaDiagnostics.keyspaceDropped(this, keyspaceMetadata); } - - private void dropView(Keyspace keyspace, ViewMetadata metadata, boolean dropData) - { - keyspace.viewManager.dropView(metadata.name()); - dropTable(keyspace, metadata.metadata, dropData); - } - - private void dropTable(Keyspace keyspace, TableMetadata metadata, boolean dropData) - { - SchemaDiagnostics.tableDropping(this, metadata); - keyspace.dropCf(metadata.id, dropData); - SchemaDiagnostics.tableDropped(this, metadata); - } - - private void createTable(Keyspace keyspace, TableMetadata table) - { - SchemaDiagnostics.tableCreating(this, table); - keyspace.initCf(tableMetadataRefCache.getTableMetadataRef(table.id), true); - SchemaDiagnostics.tableCreated(this, table); - } - - private void createView(Keyspace keyspace, ViewMetadata view) - { - SchemaDiagnostics.tableCreating(this, view.metadata); - keyspace.initCf(tableMetadataRefCache.getTableMetadataRef(view.metadata.id), true); - SchemaDiagnostics.tableCreated(this, view.metadata); - } - - private void alterTable(Keyspace keyspace, TableMetadata updated) - { - SchemaDiagnostics.tableAltering(this, updated); - keyspace.getColumnFamilyStore(updated.name).reload(); - SchemaDiagnostics.tableAltered(this, updated); - } - - private void alterView(Keyspace keyspace, ViewMetadata updated) - { - SchemaDiagnostics.tableAltering(this, updated.metadata); - keyspace.getColumnFamilyStore(updated.name()).reload(); - SchemaDiagnostics.tableAltered(this, updated.metadata); - } - - public Map> getOutstandingSchemaVersions() - { - return updateHandler instanceof DefaultSchemaUpdateHandler - ? ((DefaultSchemaUpdateHandler) updateHandler).getOutstandingSchemaVersions() - : Collections.emptyMap(); - } - } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java index 2c36d3e35b..a6561e2522 100644 --- a/src/java/org/apache/cassandra/schema/SchemaConstants.java +++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java @@ -41,6 +41,7 @@ public final class SchemaConstants public static final String SYSTEM_KEYSPACE_NAME = "system"; public static final String SCHEMA_KEYSPACE_NAME = "system_schema"; + public static final String METADATA_KEYSPACE_NAME = "system_cluster_metadata"; public static final String TRACE_KEYSPACE_NAME = "system_traces"; public static final String AUTH_KEYSPACE_NAME = "system_auth"; @@ -62,7 +63,7 @@ public final class SchemaConstants /* replicate system keyspace names (the ones with a "true" replication strategy) */ public static final Set REPLICATED_SYSTEM_KEYSPACE_NAMES = - ImmutableSet.of(TRACE_KEYSPACE_NAME, AUTH_KEYSPACE_NAME, DISTRIBUTED_KEYSPACE_NAME); + ImmutableSet.of(TRACE_KEYSPACE_NAME, AUTH_KEYSPACE_NAME, DISTRIBUTED_KEYSPACE_NAME, METADATA_KEYSPACE_NAME); /** * The longest permissible KS or CF name. * diff --git a/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java b/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java index 29243039da..f7d0ed939f 100644 --- a/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java +++ b/src/java/org/apache/cassandra/schema/SchemaDiagnostics.java @@ -23,7 +23,8 @@ import com.google.common.collect.MapDifference; import org.apache.cassandra.diag.DiagnosticEventService; import org.apache.cassandra.schema.SchemaEvent.SchemaEventType; -final class SchemaDiagnostics +// TODO: re-wire schema diagnostics events +public final class SchemaDiagnostics { private static final DiagnosticEventService service = DiagnosticEventService.instance(); @@ -31,139 +32,140 @@ final class SchemaDiagnostics { } - static void metadataInitialized(Schema schema, KeyspaceMetadata ksmUpdate) + static void metadataInitialized(SchemaProvider schema, KeyspaceMetadata ksmUpdate) { if (isEnabled(SchemaEventType.KS_METADATA_LOADED)) service.publish(new SchemaEvent(SchemaEventType.KS_METADATA_LOADED, schema, ksmUpdate, null, null, null, null, null, null)); } - static void metadataReloaded(Schema schema, KeyspaceMetadata previous, KeyspaceMetadata ksmUpdate, Tables.TablesDiff tablesDiff, Views.ViewsDiff viewsDiff, MapDifference indexesDiff) + static void metadataReloaded(SchemaProvider schema, KeyspaceMetadata previous, KeyspaceMetadata ksmUpdate, Tables.TablesDiff tablesDiff, Views.ViewsDiff viewsDiff, MapDifference indexesDiff) { if (isEnabled(SchemaEventType.KS_METADATA_RELOADED)) service.publish(new SchemaEvent(SchemaEventType.KS_METADATA_RELOADED, schema, ksmUpdate, previous, null, null, tablesDiff, viewsDiff, indexesDiff)); } - static void metadataRemoved(Schema schema, KeyspaceMetadata ksmUpdate) + static void metadataRemoved(SchemaProvider schema, KeyspaceMetadata ksmUpdate) { if (isEnabled(SchemaEventType.KS_METADATA_REMOVED)) service.publish(new SchemaEvent(SchemaEventType.KS_METADATA_REMOVED, schema, ksmUpdate, null, null, null, null, null, null)); } - static void versionUpdated(Schema schema) + public static void versionUpdated(SchemaProvider schema) { if (isEnabled(SchemaEventType.VERSION_UPDATED)) service.publish(new SchemaEvent(SchemaEventType.VERSION_UPDATED, schema, null, null, null, null, null, null, null)); } - static void keyspaceCreating(Schema schema, KeyspaceMetadata keyspace) + static void keyspaceCreating(SchemaProvider schema, KeyspaceMetadata keyspace) { if (isEnabled(SchemaEventType.KS_CREATING)) service.publish(new SchemaEvent(SchemaEventType.KS_CREATING, schema, keyspace, null, null, null, null, null, null)); } - static void keyspaceCreated(Schema schema, KeyspaceMetadata keyspace) + static void keyspaceCreated(SchemaProvider schema, KeyspaceMetadata keyspace) { if (isEnabled(SchemaEventType.KS_CREATED)) service.publish(new SchemaEvent(SchemaEventType.KS_CREATED, schema, keyspace, null, null, null, null, null, null)); } - static void keyspaceAltering(Schema schema, KeyspaceMetadata.KeyspaceDiff delta) + static void keyspaceAltering(SchemaProvider schema, KeyspaceMetadata.KeyspaceDiff delta) { if (isEnabled(SchemaEventType.KS_ALTERING)) service.publish(new SchemaEvent(SchemaEventType.KS_ALTERING, schema, delta.after, delta.before, delta, null, null, null, null)); } - static void keyspaceAltered(Schema schema, KeyspaceMetadata.KeyspaceDiff delta) + static void keyspaceAltered(SchemaProvider schema, KeyspaceMetadata.KeyspaceDiff delta) { if (isEnabled(SchemaEventType.KS_ALTERED)) service.publish(new SchemaEvent(SchemaEventType.KS_ALTERED, schema, delta.after, delta.before, delta, null, null, null, null)); } - static void keyspaceDropping(Schema schema, KeyspaceMetadata keyspace) + static void keyspaceDropping(SchemaProvider schema, KeyspaceMetadata keyspace) { if (isEnabled(SchemaEventType.KS_DROPPING)) service.publish(new SchemaEvent(SchemaEventType.KS_DROPPING, schema, keyspace, null, null, null, null, null, null)); } - static void keyspaceDropped(Schema schema, KeyspaceMetadata keyspace) + static void keyspaceDropped(SchemaProvider schema, KeyspaceMetadata keyspace) { if (isEnabled(SchemaEventType.KS_DROPPED)) service.publish(new SchemaEvent(SchemaEventType.KS_DROPPED, schema, keyspace, null, null, null, null, null, null)); } - static void schemaLoading(Schema schema) + static void schemaLoading(SchemaProvider schema) { if (isEnabled(SchemaEventType.SCHEMATA_LOADING)) service.publish(new SchemaEvent(SchemaEventType.SCHEMATA_LOADING, schema, null, null, null, null, null, null, null)); } - static void schemaLoaded(Schema schema) + // Looks like the author of the patch has meant "schemas" or "schemata", plural. So unless we rename "schemata" everywhere, i'd just leave it as-is + static void schemaLoaded(SchemaProvider schema) { if (isEnabled(SchemaEventType.SCHEMATA_LOADED)) service.publish(new SchemaEvent(SchemaEventType.SCHEMATA_LOADED, schema, null, null, null, null, null, null, null)); } - static void versionAnnounced(Schema schema) + static void versionAnnounced(SchemaProvider schema) { if (isEnabled(SchemaEventType.VERSION_ANOUNCED)) service.publish(new SchemaEvent(SchemaEventType.VERSION_ANOUNCED, schema, null, null, null, null, null, null, null)); } - static void schemaCleared(Schema schema) + static void schemaCleared(SchemaProvider schema) { if (isEnabled(SchemaEventType.SCHEMATA_CLEARED)) service.publish(new SchemaEvent(SchemaEventType.SCHEMATA_CLEARED, schema, null, null, null, null, null, null, null)); } - static void tableCreating(Schema schema, TableMetadata table) + static void tableCreating(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_CREATING)) service.publish(new SchemaEvent(SchemaEventType.TABLE_CREATING, schema, null, null, null, table, null, null, null)); } - static void tableCreated(Schema schema, TableMetadata table) + static void tableCreated(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_CREATED)) service.publish(new SchemaEvent(SchemaEventType.TABLE_CREATED, schema, null, null, null, table, null, null, null)); } - static void tableAltering(Schema schema, TableMetadata table) + static void tableAltering(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_ALTERING)) service.publish(new SchemaEvent(SchemaEventType.TABLE_ALTERING, schema, null, null, null, table, null, null, null)); } - static void tableAltered(Schema schema, TableMetadata table) + static void tableAltered(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_ALTERED)) service.publish(new SchemaEvent(SchemaEventType.TABLE_ALTERED, schema, null, null, null, table, null, null, null)); } - static void tableDropping(Schema schema, TableMetadata table) + static void tableDropping(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_DROPPING)) service.publish(new SchemaEvent(SchemaEventType.TABLE_DROPPING, schema, null, null, null, table, null, null, null)); } - static void tableDropped(Schema schema, TableMetadata table) + static void tableDropped(SchemaProvider schema, TableMetadata table) { if (isEnabled(SchemaEventType.TABLE_DROPPED)) service.publish(new SchemaEvent(SchemaEventType.TABLE_DROPPED, schema, null, diff --git a/src/java/org/apache/cassandra/schema/SchemaEvent.java b/src/java/org/apache/cassandra/schema/SchemaEvent.java index d374323bbd..3473c75191 100644 --- a/src/java/org/apache/cassandra/schema/SchemaEvent.java +++ b/src/java/org/apache/cassandra/schema/SchemaEvent.java @@ -30,6 +30,7 @@ import javax.annotation.Nullable; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.MapDifference; @@ -87,7 +88,7 @@ public final class SchemaEvent extends DiagnosticEvent SCHEMATA_CLEARED } - SchemaEvent(SchemaEventType type, Schema schema, @Nullable KeyspaceMetadata ksUpdate, + SchemaEvent(SchemaEventType type, SchemaProvider schema, @Nullable KeyspaceMetadata ksUpdate, @Nullable KeyspaceMetadata previous, @Nullable KeyspaceMetadata.KeyspaceDiff ksDiff, @Nullable TableMetadata tableUpdate, @Nullable Tables.TablesDiff tablesDiff, @Nullable Views.ViewsDiff viewsDiff, @Nullable MapDifference indexesDiff) @@ -101,9 +102,9 @@ public final class SchemaEvent extends DiagnosticEvent this.viewsDiff = viewsDiff; this.indexesDiff = indexesDiff; - this.keyspaces = schema.getKeyspaces().immutableCopy(); - this.nonSystemKeyspaces = schema.distributedKeyspaces().names(); - this.userKeyspaces = schema.getUserKeyspaces().immutableCopy(); + this.keyspaces = ImmutableSet.copyOf(schema.distributedAndLocalKeyspaces().names()); + this.nonSystemKeyspaces = ImmutableSet.copyOf(schema.distributedKeyspaces().names()); + this.userKeyspaces = ImmutableSet.copyOf(schema.getUserKeyspaces().names()); this.numberOfTables = schema.getNumberOfTables(); this.version = schema.getVersion(); // TODO: rename this field to reflect that the schema version we know here is stale (before the entire transformation started) diff --git a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java index f7044b55f7..56cd13ce45 100644 --- a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java +++ b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java @@ -67,7 +67,7 @@ import static org.apache.cassandra.utils.Simulate.With.GLOBAL_CLOCK; * system_schema.* tables and methods for manipulating them. * * Please notice this class is _not_ thread safe and all methods which reads or updates the data in schema keyspace - * should be accessed only from the implementation of {@link SchemaUpdateHandler} in synchronized blocks. + * should be accessed only from the implementation of {SchemaUpdateHandler} in synchronized blocks. */ @NotThreadSafe public final class SchemaKeyspace @@ -287,7 +287,7 @@ public final class SchemaKeyspace return KeyspaceMetadata.create(SchemaConstants.SCHEMA_KEYSPACE_NAME, KeyspaceParams.local(), org.apache.cassandra.schema.Tables.of(ALL_TABLE_METADATA)); } - static Collection convertSchemaDiffToMutations(KeyspacesDiff diff, long timestamp) + public static Collection convertSchemaDiffToMutations(KeyspacesDiff diff, long timestamp) { Map mutations = new HashMap<>(); @@ -361,32 +361,6 @@ public final class SchemaKeyspace ALL.forEach(table -> FBUtilities.waitOnFuture(getSchemaCFS(table).forceFlush(ColumnFamilyStore.FlushReason.INTERNALLY_FORCED))); } - /** - * Read schema from system keyspace and calculate MD5 digest of every row, resulting digest - * will be converted into UUID which would act as content-based version of the schema. - */ - public static UUID calculateSchemaDigest() - { - Digest digest = Digest.forSchema(); - for (String table : ALL) - { - ReadCommand cmd = getReadCommandForTableSchema(table); - try (ReadExecutionController executionController = cmd.executionController(); - PartitionIterator schema = cmd.executeInternal(executionController)) - { - while (schema.hasNext()) - { - try (RowIterator partition = schema.next()) - { - if (!isSystemKeyspaceSchemaPartition(partition.partitionKey())) - RowIterators.digest(partition, digest); - } - } - } - } - return UUID.nameUUIDFromBytes(digest.digest()); - } - /** * @param schemaTableName The name of the table responsible for part of the schema * @return CFS responsible to hold low-level serialized schema @@ -485,7 +459,8 @@ public final class SchemaKeyspace builder.update(Keyspaces) .row() .add(KeyspaceParams.Option.DURABLE_WRITES.toString(), params.durableWrites) - .add(KeyspaceParams.Option.REPLICATION.toString(), params.replication.asMap()); + .add(KeyspaceParams.Option.REPLICATION.toString(), + (params.replication.isMeta() ? params.replication.asNonMeta() : params.replication).asMap()); return builder; } @@ -885,7 +860,7 @@ public final class SchemaKeyspace .add("argument_names", function.argNames().stream().map((c) -> bbToString(c.bytes)).collect(toList())); } - private static String bbToString(ByteBuffer bb) + public static String bbToString(ByteBuffer bb) { try { @@ -933,14 +908,14 @@ public final class SchemaKeyspace { String query = format("SELECT keyspace_name FROM %s.%s", SchemaConstants.SCHEMA_KEYSPACE_NAME, KEYSPACES); - Keyspaces.Builder keyspaces = org.apache.cassandra.schema.Keyspaces.builder(); + Keyspaces keyspaces = org.apache.cassandra.schema.Keyspaces.NONE; for (UntypedResultSet.Row row : query(query)) { String keyspaceName = row.getString("keyspace_name"); if (!excludedKeyspaceNames.contains(keyspaceName)) - keyspaces.add(fetchKeyspace(keyspaceName)); + keyspaces = keyspaces.with(fetchKeyspace(keyspaceName)); } - return keyspaces.build(); + return keyspaces; } private static KeyspaceMetadata fetchKeyspace(String keyspaceName) @@ -960,7 +935,11 @@ public final class SchemaKeyspace UntypedResultSet.Row row = query(query, keyspaceName).one(); boolean durableWrites = row.getBoolean(KeyspaceParams.Option.DURABLE_WRITES.toString()); Map replication = row.getFrozenTextMap(KeyspaceParams.Option.REPLICATION.toString()); - return KeyspaceParams.create(durableWrites, replication); + KeyspaceParams params = KeyspaceParams.create(durableWrites, replication); + if (keyspaceName.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) + params = new KeyspaceParams(params.durableWrites, params.replication.asMeta()); + + return params; } private static Types fetchTypes(String keyspaceName) @@ -1393,7 +1372,7 @@ public final class SchemaKeyspace .collect(toSet()); } - static void applyChanges(Collection mutations) + public static void applyChanges(Collection mutations) { mutations.forEach(Mutation::apply); if (SchemaKeyspace.FLUSH_SCHEMA_TABLES) @@ -1408,10 +1387,10 @@ public final class SchemaKeyspace */ String query = format("SELECT keyspace_name FROM %s.%s WHERE keyspace_name IN ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, KEYSPACES); - Keyspaces.Builder keyspaces = org.apache.cassandra.schema.Keyspaces.builder(); + Keyspaces keyspaces = org.apache.cassandra.schema.Keyspaces.NONE; for (UntypedResultSet.Row row : query(query, new ArrayList<>(toFetch))) - keyspaces.add(fetchKeyspace(row.getString("keyspace_name"))); - return keyspaces.build(); + keyspaces = keyspaces.with(fetchKeyspace(row.getString("keyspace_name"))); + return keyspaces; } @VisibleForTesting diff --git a/src/java/org/apache/cassandra/schema/SchemaProvider.java b/src/java/org/apache/cassandra/schema/SchemaProvider.java index cbad42e530..0e34ee5509 100644 --- a/src/java/org/apache/cassandra/schema/SchemaProvider.java +++ b/src/java/org/apache/cassandra/schema/SchemaProvider.java @@ -18,29 +18,135 @@ package org.apache.cassandra.schema; -import java.util.function.Supplier; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import javax.annotation.Nullable; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.UserFunction; +import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.KeyspaceNotDefinedException; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.locator.LocalStrategy; +import org.apache.cassandra.tcm.ClusterMetadata; public interface SchemaProvider { + Set getKeyspaces(); + int getNumberOfTables(); + + ClusterMetadata submit(SchemaTransformation transformation); + + default UUID getVersion() + { + return ClusterMetadata.current().schema.getVersion(); + } + + Keyspaces localKeyspaces(); + Keyspaces distributedKeyspaces(); + Keyspaces distributedAndLocalKeyspaces(); + Keyspaces getUserKeyspaces(); + + void registerListener(SchemaChangeListener listener); + void unregisterListener(SchemaChangeListener listener); + + SchemaChangeNotifier schemaChangeNotifier(); + + Optional getIndexMetadata(String keyspace, String index); + + default TableMetadata getTableMetadata(Descriptor descriptor) + { + return getTableMetadata(descriptor.ksname, descriptor.cfname); + } + + default TableMetadataRef getTableMetadataRef(Descriptor descriptor) + { + return getTableMetadata(descriptor.ksname, descriptor.cfname).ref; + } + + default ViewMetadata getView(String keyspaceName, String viewName) + { + assert keyspaceName != null; + KeyspaceMetadata ksm = distributedKeyspaces().getNullable(keyspaceName); + return (ksm == null) ? null : ksm.views.getNullable(viewName); + } + + default Keyspaces getNonLocalStrategyKeyspaces() + { + return distributedKeyspaces().filter(keyspace -> keyspace.params.replication.klass != LocalStrategy.class); + } + + default TableMetadata validateTable(String keyspaceName, String tableName) + { + if (tableName.isEmpty()) + throw new InvalidRequestException("non-empty table is required"); + + KeyspaceMetadata keyspace = getKeyspaceMetadata(keyspaceName); + if (keyspace == null) + throw new KeyspaceNotDefinedException(String.format("keyspace %s does not exist", keyspaceName)); + + TableMetadata metadata = keyspace.getTableOrViewNullable(tableName); + if (metadata == null) + throw new InvalidRequestException(String.format("table %s does not exist", tableName)); + + return metadata; + } + + default ColumnFamilyStore getColumnFamilyStoreInstance(TableId id) + { + TableMetadata metadata = getTableMetadata(id); + if (metadata == null) + return null; + + Keyspace instance = getKeyspaceInstance(metadata.keyspace); + if (instance == null) + return null; + + return instance.hasColumnFamilyStore(metadata.id) + ? instance.getColumnFamilyStore(metadata.id) + : null; + } + + /** + * Get metadata about keyspace inner ColumnFamilies + * + * @param keyspaceName The name of the keyspace + * @return metadata about ColumnFamilies the belong to the given keyspace + */ + Iterable getTablesAndViews(String keyspaceName); + @Nullable Keyspace getKeyspaceInstance(String keyspaceName); - Keyspace maybeAddKeyspaceInstance(String keyspaceName, Supplier loadFunction); - @Nullable KeyspaceMetadata getKeyspaceMetadata(String keyspaceName); @Nullable TableMetadata getTableMetadata(TableId id); + @Nullable + default TableMetadataRef getTableMetadataRef(TableId id) + { + return getTableMetadata(id).ref; + } + @Nullable TableMetadata getTableMetadata(String keyspace, String table); + default TableMetadataRef getTableMetadataRef(String keyspace, String table) + { + return getTableMetadata(keyspace, table).ref; + } + default TableMetadata getExistingTableMetadata(TableId id) throws UnknownTableException { TableMetadata metadata = getTableMetadata(id); @@ -48,21 +154,81 @@ public interface SchemaProvider return metadata; String message = - String.format("Couldn't find table with id %s. If a table was just created, this is likely due to the schema" + String.format("Couldn't find table with id %s. If a table was just created, this is likely due to the schema " + "not being fully propagated. Please wait for schema agreement on table creation.", id); throw new UnknownTableException(message, id); } - @Nullable - TableMetadataRef getTableMetadataRef(String keyspace, String table); + /* Function helpers */ - @Nullable - TableMetadataRef getTableMetadataRef(TableId id); - - @Nullable - default TableMetadataRef getTableMetadataRef(Descriptor descriptor) + /** + * Get all function overloads with the specified name + * + * @param name fully qualified function name + * @return an empty list if the keyspace or the function name are not found; + * a non-empty collection of {@link Function} otherwise + */ + default Collection getUserFunctions(FunctionName name) { - return getTableMetadataRef(descriptor.ksname, descriptor.cfname); + if (!name.hasKeyspace()) + throw new IllegalArgumentException(String.format("Function name must be fully qualified: got %s", name)); + + KeyspaceMetadata ksm = getKeyspaceMetadata(name.keyspace); + return ksm == null + ? Collections.emptyList() + : ksm.userFunctions.get(name); + } + + /** + * Find the function with the specified name + * + * @param name fully qualified function name + * @param argTypes function argument types + * @return an empty {@link Optional} if the keyspace or the function name are not found; + * a non-empty optional of {@link Function} otherwise + */ + default Optional findFunction(FunctionName name, List> argTypes) + { + if (!name.hasKeyspace()) + throw new IllegalArgumentException(String.format("Function name must be fully quallified: got %s", name)); + + KeyspaceMetadata ksm = getKeyspaceMetadata(name.keyspace); + return ksm == null + ? Optional.empty() + : ksm.userFunctions.find(name, argTypes); + } + + /** + * Compute the largest gc grace seconds amongst all the tables + * @return the largest gcgs. + */ + default int largestGcgs() + { + return distributedAndLocalKeyspaces().stream() + .flatMap(ksm -> ksm.tables.stream()) + .mapToInt(tm -> tm.params.gcGraceSeconds) + .max() + .orElse(Integer.MIN_VALUE); + } + + // TODO: remove? + public abstract void saveSystemKeyspace(); + + /** + * Find the function with the specified name and arguments. + * + * @param name fully qualified function name + * @param argTypes function argument types + * @return an empty {@link Optional} if the keyspace or the function name are not found; + * a non-empty optional of {@link Function} otherwise + */ + default Optional findUserFunction(FunctionName name, List> argTypes) + { + if (!name.hasKeyspace()) + throw new IllegalArgumentException(String.format("Function name must be fully quallified: got %s", name)); + + return Optional.ofNullable(getKeyspaceMetadata(name.keyspace)) + .flatMap(ksm -> ksm.userFunctions.find(name, argTypes)); } } diff --git a/src/java/org/apache/cassandra/schema/SchemaPullVerbHandler.java b/src/java/org/apache/cassandra/schema/SchemaPullVerbHandler.java index 6589075ce3..02807474e5 100644 --- a/src/java/org/apache/cassandra/schema/SchemaPullVerbHandler.java +++ b/src/java/org/apache/cassandra/schema/SchemaPullVerbHandler.java @@ -17,10 +17,6 @@ */ package org.apache.cassandra.schema; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,27 +27,17 @@ import org.apache.cassandra.net.NoPayload; /** * Sends it's current schema state in form of mutations in response to the remote node's request. * Such a request is made when one of the nodes, by means of Gossip, detects schema disagreement in the ring. + * @deprecated See CEP-21 */ +@Deprecated(since = "CEP-21") public final class SchemaPullVerbHandler implements IVerbHandler { public static final SchemaPullVerbHandler instance = new SchemaPullVerbHandler(); private static final Logger logger = LoggerFactory.getLogger(SchemaPullVerbHandler.class); - private final List>> handlers = new CopyOnWriteArrayList<>(); - - public void register(Consumer> handler) - { - handlers.add(handler); - } - public void doVerb(Message message) { - logger.trace("Received schema pull request from {}", message.from()); - List>> handlers = this.handlers; - if (handlers.isEmpty()) - throw new UnsupportedOperationException("There is no handler registered for schema pull verb"); - - handlers.forEach(h -> h.accept(message)); + logger.warn("Schema pull request from {} ignored - please upgrade", message.from()); } } diff --git a/src/java/org/apache/cassandra/schema/SchemaPushVerbHandler.java b/src/java/org/apache/cassandra/schema/SchemaPushVerbHandler.java index 4f2325a3ac..eeb088c00c 100644 --- a/src/java/org/apache/cassandra/schema/SchemaPushVerbHandler.java +++ b/src/java/org/apache/cassandra/schema/SchemaPushVerbHandler.java @@ -18,9 +18,6 @@ package org.apache.cassandra.schema; import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,29 +31,17 @@ import org.apache.cassandra.net.Message; * Such happens when user makes local schema migration on one of the nodes in the ring * (which is going to act as coordinator) and that node sends (pushes) it's updated schema state * (in form of mutations) to all the alive nodes in the cluster. + * @deprecated See CEP-21 */ +@Deprecated(since = "CEP-21") public final class SchemaPushVerbHandler implements IVerbHandler> { public static final SchemaPushVerbHandler instance = new SchemaPushVerbHandler(); private static final Logger logger = LoggerFactory.getLogger(SchemaPushVerbHandler.class); - private final List>>> handlers = new CopyOnWriteArrayList<>(); - - public void register(Consumer>> handler) - { - handlers.add(handler); - } - public void doVerb(final Message> message) { - logger.trace("Received schema push request from {}", message.from()); - SchemaAnnouncementDiagnostics.schemataMutationsReceived(message.from()); - - List>>> handlers = this.handlers; - if (handlers.isEmpty()) - throw new UnsupportedOperationException("There is no handler registered for schema push verb"); - - handlers.forEach(h -> h.accept(message)); + logger.warn("Ignoring schema push request from {}, please upgrade", message.from()); } } diff --git a/src/java/org/apache/cassandra/schema/SchemaTransformation.java b/src/java/org/apache/cassandra/schema/SchemaTransformation.java index 2b020a02a0..75ab3767ab 100644 --- a/src/java/org/apache/cassandra/schema/SchemaTransformation.java +++ b/src/java/org/apache/cassandra/schema/SchemaTransformation.java @@ -17,20 +17,76 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.Optional; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; + public interface SchemaTransformation { + SchemaTransformationSerializer serializer = new SchemaTransformationSerializer(); + /** * Apply a statement transformation to a schema snapshot. *

    * Implementing methods should be side-effect free (outside of throwing exceptions if the transformation cannot * be successfully applied to the provided schema). * - * @param schema Keyspaces to base the transformation on + * @param metadata Cluster metadata representing the current state, including the DistributedSchema with the + * Keyspaces to base the transformation on * @return Keyspaces transformed by the statement */ - Keyspaces apply(Keyspaces schema); + Keyspaces apply(ClusterMetadata metadata); + + default String cql() + { + return "null"; + } + + /** + * SchemaTransformation::apply should be side effect free, as it may be called repeatedly for any given + * transformation. + *

      + *
    • On receiving a CQL DDL statement, the coordinator pre-emptively applies it using its own current cluster + * metadata for validation. The result of this transformation is discarded and the coordinator submits to the CMS. + *
    • + *
    • A CMS member, receiving a Commit containing an AlterSchema, applies the enclosed SchemaTransformation before + * committing to the metadata log. There may be multiple attempts to commit, and so multiple applications, if + * there is contention on the metadata log. + *
    • + *
    • Following commit, all peers should receive the committed log entry and they will then execute the AlterSchema, + * applying the SchemaTransformation locally. The result of this application then becomes the current published + * cluster metadata for the peer. + *
    • + *
    + * At the moment, not all SchemaTransformation are entirely free of side effects, because both client warnings and + * guardrails are tightly coupled to the ST implementations and they do produce side effects. Ideally, ClientWarn + * and Guardrails can be reworked to decouple them from ClientState and threadlocals. In the meantime, we can hint + * to them that SchemaTransformation::apply is being called in the context of AlterSchema::execute and so they + * should not produce their side effects. The default impl is a no-op, but AlterSchemaStatement which has access to + * a ClientState, is able to pause both warnings and guardrails. + * @see org.apache.cassandra.cql3.statements.schema.AlterSchemaStatement#enterExecution() + */ + default void enterExecution() + { + } + + default void exitExecution() + { + } + + default String keyspace() + { + return null; + } /** * If the transformation should be applied with a certain timestamp, this method should be overriden. This is used @@ -47,8 +103,8 @@ public interface SchemaTransformation */ class SchemaTransformationResult { - public final DistributedSchema before; - public final DistributedSchema after; + private final DistributedSchema before; + private final DistributedSchema after; public final Keyspaces.KeyspacesDiff diff; public SchemaTransformationResult(DistributedSchema before, DistributedSchema after, Keyspaces.KeyspacesDiff diff) @@ -64,4 +120,37 @@ public interface SchemaTransformation return String.format("SchemaTransformationResult{%s --> %s, diff=%s}", before.getVersion(), after.getVersion(), diff); } } + + class SchemaTransformationSerializer implements MetadataSerializer + { + public void serialize(SchemaTransformation transformation, DataOutputPlus out, Version version) throws IOException + { + boolean hasKeyspace = transformation.keyspace() != null; + out.writeBoolean(hasKeyspace); + if (hasKeyspace) + out.writeUTF(transformation.keyspace()); + out.writeUTF(transformation.cql()); + } + + public SchemaTransformation deserialize(DataInputPlus in, Version version) throws IOException + { + boolean hasKeyspace = in.readBoolean(); + String keyspace = null; + if (hasKeyspace) + keyspace = in.readUTF(); + String cql = in.readUTF(); + CQLStatement statement = QueryProcessor.getStatement(cql, ClientState.forInternalCalls(keyspace)); + if (!(statement instanceof SchemaTransformation)) + throw new IllegalArgumentException("Can not deserialize schema transformation"); + return (SchemaTransformation) statement; + } + + public long serializedSize(SchemaTransformation t, Version version) + { + long size = TypeSizes.sizeof(true); + if (t.keyspace() != null) + size += TypeSizes.sizeof(t.keyspace()); + return size + TypeSizes.sizeof(t.cql()); + } + } } diff --git a/src/java/org/apache/cassandra/schema/SchemaTransformations.java b/src/java/org/apache/cassandra/schema/SchemaTransformations.java index 124f9a6ef6..c6996199e5 100644 --- a/src/java/org/apache/cassandra/schema/SchemaTransformations.java +++ b/src/java/org/apache/cassandra/schema/SchemaTransformations.java @@ -23,6 +23,10 @@ import java.util.Optional; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; @@ -31,6 +35,14 @@ import static org.apache.cassandra.cql3.statements.RequestValidations.invalidReq */ public class SchemaTransformations { + public static SchemaTransformation fromCql(String cql) + { + CQLStatement statement = QueryProcessor.getStatement(cql, ClientState.forInternalCalls()); + if (!(statement instanceof SchemaTransformation)) + throw new IllegalArgumentException("Can not deserialize schema transformation"); + return (SchemaTransformation) statement; + } + /** * Creates a schema transformation that adds the provided keyspace. * @@ -42,8 +54,9 @@ public class SchemaTransformations */ public static SchemaTransformation addKeyspace(KeyspaceMetadata keyspace, boolean ignoreIfExists) { - return schema -> + return (metadata) -> { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata existing = schema.getNullable(keyspace.name); if (existing != null) { @@ -68,8 +81,9 @@ public class SchemaTransformations */ public static SchemaTransformation addTable(TableMetadata table, boolean ignoreIfExists) { - return schema -> + return (metadata) -> { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(table.keyspace); if (keyspace == null) throw invalidRequest("Keyspace '%s' doesn't exist", table.keyspace); @@ -90,8 +104,9 @@ public class SchemaTransformations public static SchemaTransformation addTypes(Types toAdd, boolean ignoreIfExists) { - return schema -> + return (metadata) -> { + Keyspaces schema = metadata.schema.getKeyspaces(); if (toAdd.isEmpty()) return schema; @@ -128,8 +143,9 @@ public class SchemaTransformations */ public static SchemaTransformation addView(ViewMetadata view, boolean ignoreIfExists) { - return schema -> + return (metadata) -> { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata keyspace = schema.getNullable(view.keyspace()); if (keyspace == null) throw invalidRequest("Cannot add view to non existing keyspace '%s'", view.keyspace()); @@ -169,8 +185,9 @@ public class SchemaTransformations } @Override - public Keyspaces apply(Keyspaces schema) + public Keyspaces apply(ClusterMetadata metadata) { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata updatedKeyspace = keyspace; KeyspaceMetadata curKeyspace = schema.getNullable(keyspace.name); if (curKeyspace != null) @@ -207,4 +224,4 @@ public class SchemaTransformations }; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/schema/SchemaUpdateHandler.java b/src/java/org/apache/cassandra/schema/SchemaUpdateHandler.java deleted file mode 100644 index f6711f3219..0000000000 --- a/src/java/org/apache/cassandra/schema/SchemaUpdateHandler.java +++ /dev/null @@ -1,78 +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.schema; - -import java.time.Duration; - -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; -import org.apache.cassandra.utils.concurrent.Awaitable; - -/** - * Schema update handler is responsible for maintaining the shared schema and synchronizing it with other nodes in - * the cluster, which means pushing and pulling changes, as well as tracking the current version in the cluster. - *

    - * The interface has been extracted to abstract out that functionality. It allows for various implementations like - * Gossip based (the default), ETCD, offline, etc., and make it easier for mocking in unit tests. - */ -public interface SchemaUpdateHandler -{ - /** - * Starts actively synchronizing schema with the rest of the cluster. It is called in the very beginning of the - * node startup. It is not expected to block - to await for the startup completion we have another method - * {@link #waitUntilReady(Duration)}. - */ - void start(); - - /** - * Waits until the schema update handler is ready and returns the result. If the method returns {@code false} it - * means that readiness could not be achieved within the specified period of time. The method can be used just to - * check if schema is ready by passing {@link Duration#ZERO} as the timeout - in such case it returns immediately. - * - * @param timeout the maximum time to wait for schema readiness - * @return whether readiness is achieved - */ - boolean waitUntilReady(Duration timeout); - - /** - * Applies schema transformation in the underlying storage and synchronizes with other nodes. - * - * @param transformation schema transformation to be performed - * @param local if true, the caller does not require synchronizing schema with other nodes - in practise local is - * used only in some tests - * @return transformation result - */ - SchemaTransformationResult apply(SchemaTransformation transformation, boolean local); - - /** - * Resets the schema either by reloading data from the local storage or from the other nodes. Once the schema is - * refreshed, the callbacks provided in the factory method are executed, and the updated schema version is announced. - * - * @param local whether we should reset with locally stored schema or fetch the schema from other nodes - */ - void reset(boolean local); - - /** - * Marks the local schema to be cleared and refreshed. Since calling this method, the update handler tries to obtain - * a fresh schema definition from a remote source. Once the schema definition is received, the local schema is - * replaced (instead of being merged which usually happens when the update is received). - *

    - * The returned awaitable is fulfilled when the schema is received and applied. - */ - Awaitable clear(); -} diff --git a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java deleted file mode 100644 index f324a5d6e6..0000000000 --- a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactory.java +++ /dev/null @@ -1,35 +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.schema; - -import java.util.function.BiConsumer; - -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; - -public interface SchemaUpdateHandlerFactory -{ - /** - * A factory which provides the appropriate schema update handler. The actual implementation may be different for - * different run modes (client, tool, daemon). - * - * @param online whether schema update handler should work online and be aware of the other nodes (when in daemon mode) - * @param updateSchemaCallback callback which will be called right after the shared schema is updated - */ - SchemaUpdateHandler getSchemaUpdateHandler(boolean online, BiConsumer updateSchemaCallback); -} diff --git a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java b/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java deleted file mode 100644 index 340fea2a33..0000000000 --- a/src/java/org/apache/cassandra/schema/SchemaUpdateHandlerFactoryProvider.java +++ /dev/null @@ -1,65 +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.schema; - -import javax.inject.Provider; - -import org.apache.commons.lang3.StringUtils; - -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.utils.FBUtilities; - -import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_UPDATE_HANDLER_FACTORY_CLASS; - -/** - * Provides the instance of SchemaUpdateHandler factory pointed by - * {@link org.apache.cassandra.config.CassandraRelevantProperties#SCHEMA_UPDATE_HANDLER_FACTORY_CLASS} system property. - * If the property is not defined, the default factory {@link DefaultSchemaUpdateHandler} instance is returned. - */ -public class SchemaUpdateHandlerFactoryProvider implements Provider -{ - /** @deprecated Use CassandraRelevantProperties.SCHEMA_UPDATE_HANDLER_FACTORY_CLASS instead. See CASSANDRA-17797 */ - @Deprecated(since = "5.0") - public static final String SUH_FACTORY_CLASS_PROPERTY = "cassandra.schema.update_handler_factory.class"; - - public final static SchemaUpdateHandlerFactoryProvider instance = new SchemaUpdateHandlerFactoryProvider(); - - @Override - public SchemaUpdateHandlerFactory get() - { - String suhFactoryClassName = StringUtils.trimToNull(SCHEMA_UPDATE_HANDLER_FACTORY_CLASS.getString()); - if (suhFactoryClassName == null) - { - return DefaultSchemaUpdateHandlerFactory.instance; - } - else - { - Class suhFactoryClass = FBUtilities.classForName(suhFactoryClassName, "schema update handler factory"); - try - { - return suhFactoryClass.newInstance(); - } - catch (InstantiationException | IllegalAccessException ex) - { - throw new ConfigurationException(String.format("Failed to initialize schema update handler factory class %s defined in %s system property.", - suhFactoryClassName, SCHEMA_UPDATE_HANDLER_FACTORY_CLASS.getKey()), ex); - } - } - } -} diff --git a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java index e97e6ca00a..08640007df 100644 --- a/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java +++ b/src/java/org/apache/cassandra/schema/SystemDistributedKeyspace.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -49,7 +50,6 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.repair.CommonRange; import org.apache.cassandra.repair.messages.RepairOption; @@ -68,7 +68,7 @@ public final class SystemDistributedKeyspace public static final String NAME = "system_distributed"; - private static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_DISTRIBUTED_DEFAULT_RF.getInt(); + public static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_DISTRIBUTED_DEFAULT_RF.getInt(); private static final Logger logger = LoggerFactory.getLogger(SystemDistributedKeyspace.class); /** @@ -83,6 +83,8 @@ public final class SystemDistributedKeyspace * gen 4: compression chunk length reduced to 16KiB, memtable_flush_period_in_ms now unset on all tables in 4.0 * gen 5: add ttl and TWCS to repair_history tables * gen 6: add denylist table + * + * // TODO: TCM - how do we evolve these tables? */ public static final long GENERATION = 6; @@ -95,71 +97,67 @@ public final class SystemDistributedKeyspace public static final String PARTITION_DENYLIST_TABLE = "partition_denylist"; public static final Set TABLE_NAMES = ImmutableSet.of(REPAIR_HISTORY, PARENT_REPAIR_HISTORY, VIEW_BUILD_STATUS, PARTITION_DENYLIST_TABLE); - + + public static final String REPAIR_HISTORY_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "keyspace_name text," + + "columnfamily_name text," + + "id timeuuid," + + "parent_id timeuuid," + + "range_begin text," + + "range_end text," + + "coordinator inet," + + "coordinator_port int," + + "participants set," + + "participants_v2 set," + + "exception_message text," + + "exception_stacktrace text," + + "status text," + + "started_at timestamp," + + "finished_at timestamp," + + "PRIMARY KEY ((keyspace_name, columnfamily_name), id))"; + private static final TableMetadata RepairHistory = - parse(REPAIR_HISTORY, - "Repair history", - "CREATE TABLE %s (" - + "keyspace_name text," - + "columnfamily_name text," - + "id timeuuid," - + "parent_id timeuuid," - + "range_begin text," - + "range_end text," - + "coordinator inet," - + "coordinator_port int," - + "participants set," - + "participants_v2 set," - + "exception_message text," - + "exception_stacktrace text," - + "status text," - + "started_at timestamp," - + "finished_at timestamp," - + "PRIMARY KEY ((keyspace_name, columnfamily_name), id))") + parse(REPAIR_HISTORY, "Repair history", REPAIR_HISTORY_CQL) .defaultTimeToLive((int) TimeUnit.DAYS.toSeconds(30)) .compaction(CompactionParams.twcs(ImmutableMap.of("compaction_window_unit","DAYS", "compaction_window_size","1"))) .build(); + public static final String PARENT_REPAIR_HISTORY_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "parent_id timeuuid," + + "keyspace_name text," + + "columnfamily_names set," + + "started_at timestamp," + + "finished_at timestamp," + + "exception_message text," + + "exception_stacktrace text," + + "requested_ranges set," + + "successful_ranges set," + + "options map," + + "PRIMARY KEY (parent_id))"; private static final TableMetadata ParentRepairHistory = - parse(PARENT_REPAIR_HISTORY, - "Repair history", - "CREATE TABLE %s (" - + "parent_id timeuuid," - + "keyspace_name text," - + "columnfamily_names set," - + "started_at timestamp," - + "finished_at timestamp," - + "exception_message text," - + "exception_stacktrace text," - + "requested_ranges set," - + "successful_ranges set," - + "options map," - + "PRIMARY KEY (parent_id))") + parse(PARENT_REPAIR_HISTORY, "Repair history", PARENT_REPAIR_HISTORY_CQL) .defaultTimeToLive((int) TimeUnit.DAYS.toSeconds(30)) .compaction(CompactionParams.twcs(ImmutableMap.of("compaction_window_unit","DAYS", "compaction_window_size","1"))) .build(); + public static final String VIEW_BUILD_STATUS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "keyspace_name text," + + "view_name text," + + "host_id uuid," + + "status text," + + "PRIMARY KEY ((keyspace_name, view_name), host_id))"; private static final TableMetadata ViewBuildStatus = - parse(VIEW_BUILD_STATUS, - "Materialized View build status", - "CREATE TABLE %s (" - + "keyspace_name text," - + "view_name text," - + "host_id uuid," - + "status text," - + "PRIMARY KEY ((keyspace_name, view_name), host_id))").build(); + parse(VIEW_BUILD_STATUS, "Materialized View build status", VIEW_BUILD_STATUS_CQL).build(); - public static final TableMetadata PartitionDenylistTable = - parse(PARTITION_DENYLIST_TABLE, - "Partition keys which have been denied access", - "CREATE TABLE %s (" - + "ks_name text," - + "table_name text," - + "key blob," - + "PRIMARY KEY ((ks_name, table_name), key))") - .build(); + public static final String PARTITION_DENYLIST_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "ks_name text," + + "table_name text," + + "key blob," + + "PRIMARY KEY ((ks_name, table_name), key))"; + private static final TableMetadata PartitionDenylistTable = + parse(PARTITION_DENYLIST_TABLE, "Partition keys which have been denied access", PARTITION_DENYLIST_CQL).build(); private static TableMetadata.Builder parse(String table, String description, String cql) { @@ -168,9 +166,12 @@ public final class SystemDistributedKeyspace .comment(description); } + @VisibleForTesting public static KeyspaceMetadata metadata() { - return KeyspaceMetadata.create(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus, PartitionDenylistTable)); + return KeyspaceMetadata.create(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())), + Tables.of(RepairHistory, ParentRepairHistory, ViewBuildStatus, PartitionDenylistTable)); } public static void startParentRepair(TimeUUID parent_id, String keyspaceName, String[] cfnames, RepairOption options) @@ -228,10 +229,6 @@ public final class SystemDistributedKeyspace public static void startRepairs(TimeUUID id, TimeUUID parent_id, String keyspaceName, String[] cfnames, CommonRange commonRange) { - // Don't record repair history if an upgrade is in progress as version 3 nodes generates errors - // due to schema differences - boolean includeNewColumns = !Gossiper.instance.hasMajorVersion3Nodes(); - InetAddressAndPort coordinator = FBUtilities.getBroadcastAddressAndPort(); Set participants = Sets.newHashSet(); Set participants_v2 = Sets.newHashSet(); @@ -254,34 +251,18 @@ public final class SystemDistributedKeyspace for (Range range : commonRange.ranges) { String fmtQry; - if (includeNewColumns) - { - fmtQry = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY, - keyspaceName, - cfname, - id.toString(), - parent_id.toString(), - range.left.toString(), - range.right.toString(), - coordinator.getHostAddress(false), - coordinator.getPort(), - Joiner.on("', '").join(participants), - Joiner.on("', '").join(participants_v2), - RepairState.STARTED.toString()); - } - else - { - fmtQry = format(queryWithoutNewColumns, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY, - keyspaceName, - cfname, - id.toString(), - parent_id.toString(), - range.left.toString(), - range.right.toString(), - coordinator.getHostAddress(false), - Joiner.on("', '").join(participants), - RepairState.STARTED.toString()); - } + fmtQry = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, REPAIR_HISTORY, + keyspaceName, + cfname, + id.toString(), + parent_id.toString(), + range.left.toString(), + range.right.toString(), + coordinator.getHostAddress(false), + coordinator.getPort(), + Joiner.on("', '").join(participants), + Joiner.on("', '").join(participants_v2), + RepairState.STARTED.toString()); processSilent(fmtQry); } } diff --git a/src/java/org/apache/cassandra/schema/TableId.java b/src/java/org/apache/cassandra/schema/TableId.java index ceeeec315a..69a853d45a 100644 --- a/src/java/org/apache/cassandra/schema/TableId.java +++ b/src/java/org/apache/cassandra/schema/TableId.java @@ -25,14 +25,14 @@ import java.util.UUID; import javax.annotation.Nullable; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.commons.lang3.ArrayUtils; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; -import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; - import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; /** * The unique identifier of a table. @@ -40,8 +40,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; * This is essentially a UUID, but we wrap it as it's used quite a bit in the code and having a nicely named class make * the code more readable. */ -public class TableId +public class TableId implements Comparable { + public static final long MAGIC = 1956074401491665062L; // TODO: should this be a TimeUUID? private final UUID id; @@ -66,6 +67,23 @@ public class TableId return new TableId(UUID.fromString(idString)); } + public static TableId get(ClusterMetadata metadata) + { + int i = 0; + while (true) + { + TableId tableId = TableId.fromLong(metadata.epoch.getEpoch() + i); + if (!tableIdExists(metadata, tableId)) + return tableId; + i++; + } + } + + private static boolean tableIdExists(ClusterMetadata metadata, TableId tableId) + { + return metadata.schema.getKeyspaces().stream().anyMatch(ks -> ks.tables.containsTable(tableId)); + } + @Nullable public static Pair tableNameAndIdFromFilename(String filename) { @@ -87,6 +105,11 @@ public class TableId return fromUUID(new UUID(msb, lsb)); } + private static TableId fromLong(long start) + { + return TableId.fromUUID(new UUID(MAGIC, start)); + } + /** * Creates the UUID of a system table. * @@ -149,4 +172,10 @@ public class TableId { return new TableId(new UUID(in.readLong(), in.readLong())); } + + @Override + public int compareTo(TableId o) + { + return id.compareTo(o.id); + } } diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java index b79e80c4e5..5f60250649 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadata.java +++ b/src/java/org/apache/cassandra/schema/TableMetadata.java @@ -17,11 +17,13 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -59,13 +61,19 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.db.marshal.EmptyType; -import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; import org.github.jamm.Unmetered; import static com.google.common.collect.Iterables.any; @@ -73,11 +81,14 @@ import static com.google.common.collect.Iterables.transform; import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import static org.apache.cassandra.db.TypeSizes.sizeof; import static org.apache.cassandra.schema.IndexMetadata.isNameValid; @Unmetered public class TableMetadata implements SchemaElement { + public static final Serializer serializer = new Serializer(); + private static final Logger logger = LoggerFactory.getLogger(TableMetadata.class); // Please note that currently the only one truly useful flag is COUNTER, as the rest of the flags were about @@ -151,6 +162,7 @@ public class TableMetadata implements SchemaElement public final String keyspace; public final String name; public final TableId id; + public final Epoch epoch; public final IPartitioner partitioner; public final Kind kind; @@ -182,6 +194,7 @@ public class TableMetadata implements SchemaElement // performance hacks; TODO see if all are really necessary public final DataResource resource; + public TableMetadataRef ref; protected TableMetadata(Builder builder) { @@ -189,7 +202,7 @@ public class TableMetadata implements SchemaElement keyspace = builder.keyspace; name = builder.name; id = builder.id; - + epoch = builder.epoch; partitioner = builder.partitioner; kind = builder.kind; params = builder.params.build(); @@ -214,6 +227,14 @@ public class TableMetadata implements SchemaElement comparator = new ClusteringComparator(transform(clusteringColumns, c -> c.type)); resource = DataResource.table(keyspace, name); + if (builder.isOffline) + ref = TableMetadataRef.forOfflineTools(this); + else if (SchemaConstants.isLocalSystemKeyspace(keyspace)) + ref = TableMetadataRef.forSystemTable(this); + else if (isIndex()) + ref = TableMetadataRef.forIndex(Schema.instance, this, keyspace, indexName, id); + else + ref = TableMetadataRef.withInitialReference(new TableMetadataRef(Schema.instance, keyspace, name, id), this); } public static Builder builder(String keyspace, String table) @@ -236,7 +257,8 @@ public class TableMetadata implements SchemaElement .addColumns(columns()) .droppedColumns(droppedColumns) .indexes(indexes) - .triggers(triggers); + .triggers(triggers) + .epoch(epoch); } public boolean isIndex() @@ -763,6 +785,7 @@ public class TableMetadata implements SchemaElement final String keyspace; final String name; + private Epoch epoch = Epoch.EMPTY; private TableId id; private IPartitioner partitioner; @@ -774,6 +797,8 @@ public class TableMetadata implements SchemaElement private Triggers triggers = Triggers.none(); private Indexes indexes = Indexes.none(); + private boolean isOffline = false; + private final Map droppedColumns = new HashMap<>(); private final Map columns = new HashMap<>(); private final List partitionKeyColumns = new ArrayList<>(); @@ -795,15 +820,19 @@ public class TableMetadata implements SchemaElement public TableMetadata build() { + if (keyspace == null) + throw new ConfigurationException(keyspace + '.' + name + ": Keyspace name must not be empty"); if (partitioner == null) partitioner = DatabaseDescriptor.getPartitioner(); if (id == null) { - // make sure vtables use determiniestic ids so they can be referenced in calls cross-nodes + // make sure vtables use deteriminstic ids so they can be referenced in calls cross-nodes // see CASSANDRA-17295 - if (DatabaseDescriptor.useDeterministicTableID() || kind == Kind.VIRTUAL) id = TableId.unsafeDeterministic(keyspace, name); - else id = TableId.generate(); + if (DatabaseDescriptor.useDeterministicTableID() || kind == Kind.VIRTUAL) + id = TableId.unsafeDeterministic(keyspace, name); + else + id = TableId.generate(); } if (Flag.isCQLTable(flags)) @@ -818,6 +847,17 @@ public class TableMetadata implements SchemaElement return this; } + public boolean hasId() + { + return id != null; + } + + public Builder epoch(Epoch val) + { + epoch = val; + return this; + } + public Builder partitioner(IPartitioner val) { partitioner = val; @@ -1209,6 +1249,12 @@ public class TableMetadata implements SchemaElement columns.put(column.name.bytes, newColumn); } + + public Builder offline() + { + this.isOffline = true; + return this; + } } /** @@ -1665,14 +1711,6 @@ public class TableMetadata implements SchemaElement return clusteringColumns.get(0).type; } - public AbstractType columnDefinitionNameComparator(ColumnMetadata.Kind kind) - { - return (Flag.isSuper(this.flags) && kind == ColumnMetadata.Kind.REGULAR) || - (isStaticCompactTable() && kind == ColumnMetadata.Kind.STATIC) - ? staticCompactOrSuperTableColumnNameType() - : UTF8Type.instance; - } - @Override public boolean isStaticCompactTable() { @@ -1716,7 +1754,110 @@ public class TableMetadata implements SchemaElement assert columns.regulars.simpleColumnCount() == 1 && columns.regulars.complexColumnCount() == 0; return columns.regulars.getSimple(0); } - } + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(TableMetadata t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.keyspace); + out.writeUTF(t.name); + + out.writeBoolean(t.epoch != null); + if (t.epoch != null) + Epoch.serializer.serialize(t.epoch, out, version); + + t.id.serialize(out); + out.writeUTF(t.partitioner.getClass().getCanonicalName()); + out.writeUTF(t.kind.name()); + TableParams.serializer.serialize(t.params, out, version); + + out.writeInt(t.flags.size()); + for (Flag f : t.flags) + out.writeUTF(f.name()); + + out.writeInt(t.columns().size()); + for (ColumnMetadata cm : t.columns()) + ColumnMetadata.serializer.serialize(cm, out, version); + + out.writeInt(t.droppedColumns.size()); + for (Entry e: t.droppedColumns.entrySet()) + { + ByteBufferUtil.writeWithShortLength(e.getKey(), out); + DroppedColumn.serializer.serialize(e.getValue(), out, version); + } + + Indexes.serializer.serialize(t.indexes, out, version); + Triggers.serializer.serialize(t.triggers, out, version); + } + + public TableMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + String ks = in.readUTF(); + String name = in.readUTF(); + + boolean hasEpoch = in.readBoolean(); + Epoch epoch = null; + if (hasEpoch) + epoch = Epoch.serializer.deserialize(in, version); + + TableId tableId = TableId.deserialize(in); + TableMetadata.Builder builder = TableMetadata.builder(ks, name, tableId); + builder.epoch(epoch); + builder.partitioner(FBUtilities.newPartitioner(in.readUTF())); + builder.kind(Kind.valueOf(in.readUTF())); + builder.params(TableParams.serializer.deserialize(in, version)); + int flagCount = in.readInt(); + Set flags = new HashSet<>(); + for (int i = 0; i < flagCount; i++) + flags.add(Flag.valueOf(in.readUTF())); + builder.flags(flags); + int columnCount = in.readInt(); + for (int i = 0; i < columnCount; i++) + builder.addColumn(ColumnMetadata.serializer.deserialize(in, types, functions, version)); + int droppedColCount = in.readInt(); + Map droppedColumns = new HashMap<>(); + for (int i = 0; i < droppedColCount; i++) + droppedColumns.put(ByteBufferUtil.readWithShortLength(in), DroppedColumn.serializer.deserialize(in, types, functions, version)); + builder.droppedColumns(droppedColumns); + builder.indexes(Indexes.serializer.deserialize(in, version)); + builder.triggers(Triggers.serializer.deserialize(in, version)); + return builder.build(); + } + + public long serializedSize(TableMetadata t, Version version) + { + long size = sizeof(t.keyspace) + + sizeof(t.name) + + t.id.serializedSize() + + sizeof(t.partitioner.getClass().getCanonicalName()) + + sizeof(t.kind.name()) + + TableParams.serializer.serializedSize(t.params, version); + + size += sizeof(t.epoch != null); + if (t.epoch != null) + size += Epoch.serializer.serializedSize(t.epoch, version); + + size += sizeof(t.flags.size()); + for (Flag f : t.flags) + size += sizeof(f.name()); + + size += sizeof(t.columns.size()); + for (ColumnMetadata cm : t.columns()) + size += ColumnMetadata.serializer.serializedSize(cm, version); + + size += sizeof(t.droppedColumns.size()); + for (Entry e : t.droppedColumns.entrySet()) + { + size += ByteBufferUtil.serializedSizeWithShortLength(e.getKey()); + size += DroppedColumn.serializer.serializedSize(e.getValue(), version); + } + + size += Indexes.serializer.serializedSize(t.indexes, version); + size += Triggers.serializer.serializedSize(t.triggers, version); + + return size; + + } + } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/schema/TableMetadataRef.java b/src/java/org/apache/cassandra/schema/TableMetadataRef.java index 6d6958edd5..92a17f27da 100644 --- a/src/java/org/apache/cassandra/schema/TableMetadataRef.java +++ b/src/java/org/apache/cassandra/schema/TableMetadataRef.java @@ -15,32 +15,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.cassandra.schema; -import org.github.jamm.Unmetered; +import java.util.Optional; -/** - * Encapsulates a volatile reference to an immutable {@link TableMetadata} instance. - * - * Used in classes that need up-to-date metadata to avoid the cost of looking up {@link Schema} hashmaps. - */ -@Unmetered -public final class TableMetadataRef +import org.apache.cassandra.cql3.ColumnIdentifier; + +import static java.lang.String.format; + +public class TableMetadataRef { + private final SchemaProvider schema; public final TableId id; public final String keyspace; public final String name; - private volatile TableMetadata metadata; private volatile TableMetadata localTableMetadata; - TableMetadataRef(TableMetadata metadata) + public static TableMetadataRef forIndex(SchemaProvider schema, TableMetadata initial, String keyspace, String name, TableId id) { - this.metadata = metadata; + return new TableMetadataRef(schema, keyspace, name, id) + { + @Override + public TableMetadata get() + { + return getOrDefault(initial); + } - id = metadata.id; - keyspace = metadata.keyspace; - name = metadata.name; + @Override + public TableMetadata getOrDefault(TableMetadata dflt) + { + Optional metadata = schema.getIndexMetadata(keyspace, name); + return metadata.orElse(dflt); + } + }; + } + + // takes a default metadata instance to provide a ref which can work before the Table has been added to + // schema. This is used when creating the initial memtable for a new CFS instance during a schema change + // event as the CFS is created before the updated schema is published. + public static TableMetadataRef withInitialReference(TableMetadataRef wrapped, TableMetadata initial) + { + return new TableMetadataRef(Schema.instance, initial.keyspace, initial.name, initial.id) + { + @Override + public TableMetadata get() + { + return wrapped.getOrDefault(initial); + } + + @Override + public TableMetadata getOrDefault(TableMetadata dflt) + { + return wrapped.getOrDefault(dflt); + } + }; } /** @@ -51,39 +81,78 @@ public final class TableMetadataRef */ public static TableMetadataRef forOfflineTools(TableMetadata metadata) { - return new TableMetadataRef(metadata); + return new TableMetadataRef(null, metadata.keyspace, metadata.name, metadata.id) + { + @Override + public TableMetadata get() + { + return metadata; + } + }; + } + + public static TableMetadataRef forSystemTable(TableMetadata metadata) + { + return new TableMetadataRef(null, metadata.keyspace, metadata.name, metadata.id) { + @Override + public TableMetadata get() + { + return metadata; + } + + @Override + public TableMetadata getOrDefault(TableMetadata dflt) + { + return metadata; + } + }; + } + + public TableMetadataRef(SchemaProvider schema, String keyspace, String name, TableId id) + { + this.schema = schema; + this.keyspace = keyspace; + this.name = name; + this.id = id; } public TableMetadata get() { + TableMetadata metadata = schema.getTableMetadata(keyspace, name); + if (metadata == null) + throw new IllegalStateException(format("Can't deref metadata for %s.%s.", keyspace, name)); return metadata; } + public TableMetadata getOrDefault(TableMetadata dflt) + { + TableMetadata tableMetadata = schema.getTableMetadata(keyspace, name); + + if (tableMetadata == null) + return dflt; + + return tableMetadata; + } + /** * Returns node-local table metadata */ public TableMetadata getLocal() { if (this.localTableMetadata != null) + { + TableMetadata global = get(); + if (!this.localTableMetadata.epoch.equals(global.epoch)) + { + this.localTableMetadata = null; + return global; + } return localTableMetadata; + } - return metadata; + return get(); } - /** - * Update the reference with the most current version of {@link TableMetadata} - *

    - * Must only be used by methods in {@link Schema}, *DO NOT* make public - * even for testing purposes, it isn't safe. - */ - void set(TableMetadata metadata) - { - metadata.validateCompatibility(get()); - this.metadata = metadata; - this.localTableMetadata = null; - } - - public void setLocalOverrides(TableMetadata metadata) { metadata.validateCompatibility(get()); @@ -93,6 +162,6 @@ public final class TableMetadataRef @Override public String toString() { - return get().toString(); + return format("%s.%s", ColumnIdentifier.maybeQuote(keyspace), ColumnIdentifier.maybeQuote(name)); } } diff --git a/src/java/org/apache/cassandra/schema/TableMetadataRefCache.java b/src/java/org/apache/cassandra/schema/TableMetadataRefCache.java deleted file mode 100644 index d947d1d690..0000000000 --- a/src/java/org/apache/cassandra/schema/TableMetadataRefCache.java +++ /dev/null @@ -1,152 +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.schema; - -import java.util.Collections; -import java.util.Map; - -import com.google.common.collect.MapDifference; -import com.google.common.collect.Maps; - -import org.apache.cassandra.utils.Pair; - -/** - * Manages the cached {@link TableMetadataRef} objects which holds the references to {@link TableMetadata} objects. - *

    - * The purpose of {@link TableMetadataRef} is that the reference to {@link TableMetadataRef} remains unchanged when - * the metadata of the table changes. {@link TableMetadata} is immutable, so when it changes, we only switch - * the reference inside the existing {@link TableMetadataRef} object. - */ -class TableMetadataRefCache -{ - public final static TableMetadataRefCache EMPTY = new TableMetadataRefCache(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap()); - - // UUID -> mutable metadata ref map. We have to update these in place every time a table changes. - private final Map metadataRefs; - - // keyspace and table names -> mutable metadata ref map. - private final Map, TableMetadataRef> metadataRefsByName; - - // (keyspace name, index name) -> mutable metadata ref map. We have to update these in place every time an index changes. - private final Map, TableMetadataRef> indexMetadataRefs; - - public TableMetadataRefCache(Map metadataRefs, - Map, TableMetadataRef> metadataRefsByName, - Map, TableMetadataRef> indexMetadataRefs) - { - this.metadataRefs = Collections.unmodifiableMap(metadataRefs); - this.metadataRefsByName = Collections.unmodifiableMap(metadataRefsByName); - this.indexMetadataRefs = Collections.unmodifiableMap(indexMetadataRefs); - } - - /** - * Returns cache copy with added the {@link TableMetadataRef} corresponding to the provided keyspace to {@link #metadataRefs} and - * {@link #indexMetadataRefs}, assuming the keyspace is new (in the sense of not being tracked by the manager yet). - */ - TableMetadataRefCache withNewRefs(KeyspaceMetadata ksm) - { - return withUpdatedRefs(ksm.empty(), ksm); - } - - /** - * Returns cache copy with updated the {@link TableMetadataRef} in {@link #metadataRefs} and {@link #indexMetadataRefs}, - * for an existing updated keyspace given it's previous and new definition. - *

    - * Note that {@link TableMetadataRef} are not duplicated and table metadata is altered in the existing refs. - */ - TableMetadataRefCache withUpdatedRefs(KeyspaceMetadata previous, KeyspaceMetadata updated) - { - Tables.TablesDiff tablesDiff = Tables.diff(previous.tables, updated.tables); - Views.ViewsDiff viewsDiff = Views.diff(previous.views, updated.views); - - MapDifference indexesDiff = previous.tables.indexesDiff(updated.tables); - - boolean hasCreatedOrDroppedTablesOrViews = tablesDiff.created.size() > 0 || tablesDiff.dropped.size() > 0 || viewsDiff.created.size() > 0 || viewsDiff.dropped.size() > 0; - boolean hasCreatedOrDroppedIndexes = !indexesDiff.entriesOnlyOnRight().isEmpty() || !indexesDiff.entriesOnlyOnLeft().isEmpty(); - - Map metadataRefs = hasCreatedOrDroppedTablesOrViews ? Maps.newHashMap(this.metadataRefs) : this.metadataRefs; - Map, TableMetadataRef> metadataRefsByName = hasCreatedOrDroppedTablesOrViews ? Maps.newHashMap(this.metadataRefsByName) : this.metadataRefsByName; - Map, TableMetadataRef> indexMetadataRefs = hasCreatedOrDroppedIndexes ? Maps.newHashMap(this.indexMetadataRefs) : this.indexMetadataRefs; - - // clean up after removed entries - tablesDiff.dropped.forEach(ref -> removeRef(metadataRefs, metadataRefsByName, ref)); - viewsDiff.dropped.forEach(view -> removeRef(metadataRefs, metadataRefsByName, view.metadata)); - indexesDiff.entriesOnlyOnLeft() - .values() - .forEach(indexTable -> indexMetadataRefs.remove(Pair.create(indexTable.keyspace, indexTable.indexName().get()))); - - // load up new entries - tablesDiff.created.forEach(table -> putRef(metadataRefs, metadataRefsByName, new TableMetadataRef(table))); - viewsDiff.created.forEach(view -> putRef(metadataRefs, metadataRefsByName, new TableMetadataRef(view.metadata))); - indexesDiff.entriesOnlyOnRight() - .values() - .forEach(indexTable -> indexMetadataRefs.put(Pair.create(indexTable.keyspace, indexTable.indexName().get()), new TableMetadataRef(indexTable))); - - // refresh refs to updated ones - tablesDiff.altered.forEach(diff -> metadataRefs.get(diff.after.id).set(diff.after)); - viewsDiff.altered.forEach(diff -> metadataRefs.get(diff.after.metadata.id).set(diff.after.metadata)); - indexesDiff.entriesDiffering() - .values() - .stream() - .map(MapDifference.ValueDifference::rightValue) - .forEach(indexTable -> indexMetadataRefs.get(Pair.create(indexTable.keyspace, indexTable.indexName().get())).set(indexTable)); - - return new TableMetadataRefCache(metadataRefs, metadataRefsByName, indexMetadataRefs); - } - - private void putRef(Map metadataRefs, - Map, TableMetadataRef> metadataRefsByName, - TableMetadataRef ref) - { - metadataRefs.put(ref.id, ref); - metadataRefsByName.put(Pair.create(ref.keyspace, ref.name), ref); - } - - private void removeRef(Map metadataRefs, - Map, TableMetadataRef> metadataRefsByName, - TableMetadata tm) - { - metadataRefs.remove(tm.id); - metadataRefsByName.remove(Pair.create(tm.keyspace, tm.name)); - } - - /** - * Returns cache copy with removed the {@link TableMetadataRef} from {@link #metadataRefs} and {@link #indexMetadataRefs} - * for the provided (dropped) keyspace. - */ - TableMetadataRefCache withRemovedRefs(KeyspaceMetadata ksm) - { - return withUpdatedRefs(ksm, ksm.empty()); - } - - public TableMetadataRef getTableMetadataRef(TableId id) - { - return metadataRefs.get(id); - } - - public TableMetadataRef getTableMetadataRef(String keyspace, String table) - { - return metadataRefsByName.get(Pair.create(keyspace, table)); - } - - public TableMetadataRef getIndexTableMetadataRef(String keyspace, String index) - { - return indexMetadataRefs.get(Pair.create(keyspace, index)); - } -} diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java index 8f883f8f47..f279bc041f 100644 --- a/src/java/org/apache/cassandra/schema/TableParams.java +++ b/src/java/org/apache/cassandra/schema/TableParams.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; import java.util.Map.Entry; @@ -24,10 +25,15 @@ import java.util.Map.Entry; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import org.apache.cassandra.cql3.Attributes; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.service.reads.PercentileSpeculativeRetryPolicy; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; @@ -37,9 +43,11 @@ import org.apache.cassandra.utils.ByteBufferUtil; import static java.lang.String.format; import static java.util.stream.Collectors.toMap; import static org.apache.cassandra.schema.TableParams.Option.*; +import static org.apache.cassandra.db.TypeSizes.sizeof; public final class TableParams { + public static final Serializer serializer = new Serializer(); public enum Option { ALLOW_AUTO_SNAPSHOT, @@ -479,4 +487,140 @@ public final class TableParams return this; } } + + public static class Serializer implements MetadataSerializer + { + public void serialize(TableParams t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.comment); + out.writeDouble(t.bloomFilterFpChance); + out.writeDouble(t.crcCheckChance); + out.writeInt(t.gcGraceSeconds); + out.writeInt(t.defaultTimeToLive); + out.writeInt(t.memtableFlushPeriodInMs); + out.writeInt(t.minIndexInterval); + out.writeInt(t.maxIndexInterval); + out.writeUTF(t.speculativeRetry.toString()); + out.writeUTF(t.additionalWritePolicy.toString()); + if (version.isAtLeast(Version.V2)) + out.writeUTF(t.memtable.configurationKey()); + serializeMap(t.caching.asMap(), out); + serializeMap(t.compaction.asMap(), out); + serializeMap(t.compression.asMap(), out); + serializeMapBB(t.extensions, out); + out.writeBoolean(t.cdc); + out.writeUTF(t.readRepair.name()); + } + + public TableParams deserialize(DataInputPlus in, Version version) throws IOException + { + TableParams.Builder builder = TableParams.builder(); + builder.comment(in.readUTF()) + .bloomFilterFpChance(in.readDouble()) + .crcCheckChance(in.readDouble()) + .gcGraceSeconds(in.readInt()) + .defaultTimeToLive(in.readInt()) + .memtableFlushPeriodInMs(in.readInt()) + .minIndexInterval(in.readInt()) + .maxIndexInterval(in.readInt()) + .speculativeRetry(SpeculativeRetryPolicy.fromString(in.readUTF())) + .additionalWritePolicy(SpeculativeRetryPolicy.fromString(in.readUTF())) + .memtable(version.isAtLeast(Version.V2) ? MemtableParams.get(in.readUTF()) : MemtableParams.DEFAULT) + .caching(CachingParams.fromMap(deserializeMap(in))) + .compaction(CompactionParams.fromMap(deserializeMap(in))) + .compression(CompressionParams.fromMap(deserializeMap(in))) + .extensions(deserializeMapBB(in)) + .cdc(in.readBoolean()) + .readRepair(ReadRepairStrategy.fromString(in.readUTF())); + return builder.build(); + } + + public long serializedSize(TableParams t, Version version) + { + return sizeof(t.comment) + + sizeof(t.bloomFilterFpChance) + + sizeof(t.crcCheckChance) + + sizeof(t.gcGraceSeconds) + + sizeof(t.defaultTimeToLive) + + sizeof(t.memtableFlushPeriodInMs) + + sizeof(t.minIndexInterval) + + sizeof(t.maxIndexInterval) + + sizeof(t.speculativeRetry.toString()) + + sizeof(t.additionalWritePolicy.toString()) + + (version.isAtLeast(Version.V2) ? sizeof(t.memtable.configurationKey()) : 0) + + serializedSizeMap(t.caching.asMap()) + + serializedSizeMap(t.compaction.asMap()) + + serializedSizeMap(t.compression.asMap()) + + serializedSizeMapBB(t.extensions) + + sizeof(t.cdc) + + sizeof(t.readRepair.name()); + } + + private void serializeMap(Map map, DataOutputPlus out) throws IOException + { + out.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) + { + out.writeUTF(entry.getKey()); + out.writeUTF(entry.getValue()); + } + } + + private long serializedSizeMap(Map map) + { + long size = sizeof(map.size()); + for (Map.Entry entry : map.entrySet()) + { + size += sizeof(entry.getKey()); + size += sizeof(entry.getValue()); + } + return size; + } + + private void serializeMapBB(Map map, DataOutputPlus out) throws IOException + { + out.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) + { + out.writeUTF(entry.getKey()); + ByteBufferUtil.writeWithVIntLength(entry.getValue(), out); + } + } + + private long serializedSizeMapBB(Map map) + { + long size = sizeof(map.size()); + for (Map.Entry entry : map.entrySet()) + { + size += sizeof(entry.getKey()); + size += ByteBufferUtil.serializedSizeWithVIntLength(entry.getValue()); + } + return size; + } + + private Map deserializeMap(DataInputPlus in) throws IOException + { + int size = in.readInt(); + Map map = Maps.newHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) + { + String key = in.readUTF(); + String value = in.readUTF(); + map.put(key, value); + } + return map; + } + private Map deserializeMapBB(DataInputPlus in) throws IOException + { + int size = in.readInt(); + Map map = Maps.newHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) + { + String key = in.readUTF(); + ByteBuffer value = ByteBufferUtil.readWithVIntLength(in); + map.put(key, value); + } + return map; + } + } } diff --git a/src/java/org/apache/cassandra/schema/Tables.java b/src/java/org/apache/cassandra/schema/Tables.java index 0f8f6b3190..74d8a90191 100644 --- a/src/java/org/apache/cassandra/schema/Tables.java +++ b/src/java/org/apache/cassandra/schema/Tables.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Iterator; @@ -30,8 +31,13 @@ import javax.annotation.Nullable; import com.google.common.collect.*; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.index.internal.CassandraIndex; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; @@ -41,6 +47,8 @@ import static com.google.common.collect.Iterables.transform; */ public final class Tables implements Iterable { + public static final Serializer serializer = new Serializer(); + private static final Tables NONE = builder().build(); private final ImmutableMap tables; @@ -285,4 +293,34 @@ public final class Tables implements Iterable return new TablesDiff(created, dropped, altered.build()); } } + + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(Tables t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.tables.size()); + for (TableMetadata tm : t.tables.values()) + TableMetadata.serializer.serialize(tm, out, version); + } + + public Tables deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + int count = in.readInt(); + Tables.Builder builder = Tables.builder(); + for (int i = 0; i < count; i++) + { + TableMetadata tm = TableMetadata.serializer.deserialize(in, types, functions, version); + builder.add(tm); + } + return builder.build(); + } + + public long serializedSize(Tables t, Version version) + { + int size = TypeSizes.sizeof(t.tables.size()); + for (TableMetadata tm : t.tables.values()) + size += TableMetadata.serializer.serializedSize(tm, version); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/TriggerMetadata.java b/src/java/org/apache/cassandra/schema/TriggerMetadata.java index d98508130b..05f8d10416 100644 --- a/src/java/org/apache/cassandra/schema/TriggerMetadata.java +++ b/src/java/org/apache/cassandra/schema/TriggerMetadata.java @@ -18,11 +18,21 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; + import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + public final class TriggerMetadata { + public static final Serializer serializer = new Serializer(); public static final String CLASS = "class"; public final String name; @@ -70,4 +80,26 @@ public final class TriggerMetadata .add("class", classOption) .toString(); } + + public static class Serializer implements MetadataSerializer + { + public void serialize(TriggerMetadata t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.name); + out.writeUTF(t.classOption); + } + + public TriggerMetadata deserialize(DataInputPlus in, Version version) throws IOException + { + String name = in.readUTF(); + String classOption = in.readUTF(); + return new TriggerMetadata(name, classOption); + } + + public long serializedSize(TriggerMetadata t, Version version) + { + return sizeof(t.name) + + sizeof(t.classOption); + } + } } diff --git a/src/java/org/apache/cassandra/schema/Triggers.java b/src/java/org/apache/cassandra/schema/Triggers.java index 5e107229fd..7ee53eceed 100644 --- a/src/java/org/apache/cassandra/schema/Triggers.java +++ b/src/java/org/apache/cassandra/schema/Triggers.java @@ -17,15 +17,24 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.Iterator; import java.util.Optional; import com.google.common.collect.ImmutableMap; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + import static com.google.common.collect.Iterables.filter; +import static org.apache.cassandra.db.TypeSizes.sizeof; public final class Triggers implements Iterable { + public static final Serializer serializer = new Serializer(); + private final ImmutableMap triggers; private Triggers(Builder builder) @@ -151,4 +160,34 @@ public final class Triggers implements Iterable return this; } } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Triggers t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.triggers.size()); + for (TriggerMetadata tm : t.triggers.values()) + TriggerMetadata.serializer.serialize(tm, out, version); + } + + public Triggers deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + Builder builder = builder(); + for (int i = 0; i < size; i++) + { + TriggerMetadata tm = TriggerMetadata.serializer.deserialize(in, version); + builder.add(tm); + } + return builder.build(); + } + + public long serializedSize(Triggers t, Version version) + { + int size = sizeof(t.triggers.size()); + for (TriggerMetadata tm : t.triggers.values()) + size += TriggerMetadata.serializer.serializedSize(tm, version); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/Types.java b/src/java/org/apache/cassandra/schema/Types.java index 0d264c4f49..9b6738e10f 100644 --- a/src/java/org/apache/cassandra/schema/Types.java +++ b/src/java/org/apache/cassandra/schema/Types.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; import java.util.function.Predicate; @@ -32,6 +33,9 @@ import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; import static java.lang.String.format; import static java.util.stream.Collectors.toList; @@ -39,6 +43,7 @@ import static java.util.stream.Collectors.toList; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; +import static org.apache.cassandra.db.TypeSizes.sizeof; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; /** @@ -46,6 +51,8 @@ import static org.apache.cassandra.utils.ByteBufferUtil.bytes; */ public final class Types implements Iterable { + public static final Serializer serializer = new Serializer(); + private static final Types NONE = new Types(ImmutableMap.of()); private final Map types; @@ -444,4 +451,63 @@ public final class Types implements Iterable return new TypesDiff(created, dropped, altered.build()); } } + + // Not quite a MetadataSerializer as it needs the keyspace name during deserialization. + public static class Serializer + { + public void serialize(Types t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.types.size()); + for (UserType type : t.types.values()) + { + out.writeUTF(type.getNameAsString()); + List fieldNames = type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList()); + List fieldTypes = type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList()); + out.writeInt(fieldNames.size()); + for (String s : fieldNames) + out.writeUTF(s); + out.writeInt(fieldTypes.size()); + for (String s : fieldTypes) + out.writeUTF(s); + } + } + + public Types deserialize(String keyspace, DataInputPlus in, Version version) throws IOException + { + int count = in.readInt(); + Types.RawBuilder builder = Types.rawBuilder(keyspace); + for (int i = 0; i < count; i++) + { + String name = in.readUTF(); + int fieldNamesSize = in.readInt(); + List fieldNames = new ArrayList<>(fieldNamesSize); + for (int x = 0; x < fieldNamesSize; x++) + fieldNames.add(in.readUTF()); + int fieldTypeSize = in.readInt(); + List fieldTypes = new ArrayList<>(fieldTypeSize); + for (int x = 0; x < fieldTypeSize; x++) + fieldTypes.add(in.readUTF()); + builder.add(name, fieldNames, fieldTypes); + } + return builder.build(); + } + + public long serializedSize(Types t, Version version) + { + long size = sizeof(t.types.size()); + for (UserType type : t.types.values()) + { + size += sizeof(type.getNameAsString()); + List fieldNames = type.fieldNames().stream().map(FieldIdentifier::toString).collect(toList()); + List fieldTypes = type.fieldTypes().stream().map(AbstractType::asCQL3Type).map(CQL3Type::toString).collect(toList()); + size += sizeof(fieldNames.size()); + for (String s : fieldNames) + size += sizeof(s); + size += sizeof(fieldTypes.size()); + for (String s : fieldTypes) + size += sizeof(s); + } + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/UserFunctions.java b/src/java/org/apache/cassandra/schema/UserFunctions.java index b40c704d0b..027fe71bc7 100644 --- a/src/java/org/apache/cassandra/schema/UserFunctions.java +++ b/src/java/org/apache/cassandra/schema/UserFunctions.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; import java.util.function.Predicate; @@ -28,16 +29,22 @@ import com.google.common.collect.*; import org.apache.cassandra.cql3.functions.*; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import static java.util.stream.Collectors.toList; import static com.google.common.collect.Iterables.any; +import static org.apache.cassandra.db.TypeSizes.sizeof; /** * An immutable container for a keyspace's UDAs and UDFs. */ public final class UserFunctions implements Iterable { + public static final Serializer serializer = new Serializer(); public enum Filter implements Predicate { ALL, UDF, UDA; @@ -339,4 +346,61 @@ public final class UserFunctions implements Iterable return new FunctionsDiff<>(created, dropped, altered.build()); } } + + public static class Serializer implements UDTAwareMetadataSerializer + { + public void serialize(UserFunctions t, DataOutputPlus out, Version version) throws IOException + { + List udfs = t.functions.values().stream().filter(Filter.UDF).collect(Collectors.toList()); + out.writeInt(udfs.size()); + for (Function f : udfs) + { + assert f instanceof UDFunction; + UDFunction.serializer.serialize(((UDFunction) f), out, version); + } + List udas = t.functions.values().stream().filter(Filter.UDA).collect(Collectors.toList()); + out.writeInt(udas.size()); + for (Function f : udas) + { + assert f instanceof UDAggregate; + UDAggregate.serializer.serialize(((UDAggregate)f), out, version); + } + } + + public UserFunctions deserialize(DataInputPlus in, Types types, Version version) throws IOException + { + int count = in.readInt(); + List udFunctions = new ArrayList<>(count); + for (int i = 0; i < count; i++) + udFunctions.add(UDFunction.serializer.deserialize(in, types, version)); + count = in.readInt(); + List udAggregates = new ArrayList<>(count); + for (int i = 0; i < count; i++) + udAggregates.add(UDAggregate.serializer.deserialize(in, types, udFunctions, version)); + UserFunctions.Builder builder = UserFunctions.builder(); + builder.add(udFunctions); + builder.add(udAggregates); + return builder.build(); + } + + public long serializedSize(UserFunctions t, Version version) + { + List udfs = t.functions.values().stream().filter(Filter.UDF).collect(Collectors.toList()); + int size = sizeof(udfs.size()); + for (Function f : udfs) + { + assert f instanceof UDFunction; + size += UDFunction.serializer.serializedSize(((UDFunction) f), version); + } + List udas = t.functions.values().stream().filter(Filter.UDA).collect(Collectors.toList()); + size += sizeof(udas.size()); + for (Function f : udas) + { + assert f instanceof UDAggregate; + size += UDAggregate.serializer.serializedSize(((UDAggregate)f), version); + } + + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/ViewMetadata.java b/src/java/org/apache/cassandra/schema/ViewMetadata.java index df26a134e2..db8b802f78 100644 --- a/src/java/org/apache/cassandra/schema/ViewMetadata.java +++ b/src/java/org/apache/cassandra/schema/ViewMetadata.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Optional; @@ -25,12 +26,20 @@ import javax.annotation.Nullable; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.antlr.runtime.RecognitionException; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.functions.masking.ColumnMask; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; public final class ViewMetadata implements SchemaElement { + public static final Serializer serializer = new Serializer(); + public final TableId baseTableId; public final String baseTableName; @@ -238,4 +247,47 @@ public final class ViewMetadata implements SchemaElement appendCqlTo(builder, withInternals, ifNotExists); return builder.toString(); } + + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(ViewMetadata t, DataOutputPlus out, Version version) throws IOException + { + TableMetadata.serializer.serialize(t.metadata, out, version); + out.writeBoolean(t.includeAllColumns); + t.baseTableId.serialize(out); + out.writeUTF(t.whereClause.toCQLString()); + out.writeUTF(t.baseTableName); + } + + public ViewMetadata deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + TableMetadata meta = TableMetadata.serializer.deserialize(in, types, functions, version); + boolean includeAllColumns = in.readBoolean(); + TableId tableId = TableId.deserialize(in); + String whereClause = in.readUTF(); + String baseTableName = in.readUTF(); + + WhereClause wc; + try + { + wc = WhereClause.parse(whereClause); + } + catch (RecognitionException e) + { + throw new RuntimeException(String.format("Unexpected error while parsing materialized view's where clause for '%s' (got %s)", meta.name, whereClause)); + } + + return new ViewMetadata(tableId, baseTableName, includeAllColumns, wc, meta); + } + + public long serializedSize(ViewMetadata t, Version version) + { + long size = TableMetadata.serializer.serializedSize(t.metadata, version); + size += TypeSizes.sizeof(t.includeAllColumns); + size += t.baseTableId.serializedSize(); + size += TypeSizes.sizeof(t.whereClause.toCQLString()); + size += TypeSizes.sizeof(t.baseTableName); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/schema/Views.java b/src/java/org/apache/cassandra/schema/Views.java index 15d13f35af..054db8bbca 100644 --- a/src/java/org/apache/cassandra/schema/Views.java +++ b/src/java/org/apache/cassandra/schema/Views.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.schema; +import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -30,12 +31,19 @@ import javax.annotation.Nullable; import com.google.common.collect.*; import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.UDTAndFunctionsAwareMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.transform; +import static org.apache.cassandra.db.TypeSizes.sizeof; public final class Views implements Iterable { + public static final Serializer serializer = new Serializer(); + private static final Views NONE = builder().build(); private final ImmutableMap views; @@ -252,4 +260,34 @@ public final class Views implements Iterable return new ViewsDiff(created, dropped, altered.build()); } } + + public static class Serializer implements UDTAndFunctionsAwareMetadataSerializer + { + public void serialize(Views t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.views.size()); + for (ViewMetadata vm : t.views.values()) + ViewMetadata.serializer.serialize(vm, out, version); + } + + public Views deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException + { + int size = in.readInt(); + Views.Builder builder = Views.builder(); + for (int i = 0; i < size; i++) + { + ViewMetadata vm = ViewMetadata.serializer.deserialize(in, types, functions, version); + builder.put(vm); + } + return builder.build(); + } + + public long serializedSize(Views t, Version version) + { + long size = sizeof(t.views.size()); + for (ViewMetadata vm : t.views.values()) + size += ViewMetadata.serializer.serializedSize(vm, version); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java index d2c54f9f8f..62d57f5ca6 100644 --- a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java @@ -34,6 +34,7 @@ import org.apache.cassandra.db.Mutation; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.locator.ReplicaPlan.ForWrite; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.concurrent.Condition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -138,6 +139,9 @@ public abstract class AbstractWriteResponseHandler implements RequestCallback throw new WriteFailureException(replicaPlan.consistencyLevel(), ackCount(), blockFor(), writeType, this.failureReasonByEndpoint); } + + if (replicaPlan.stillAppliesTo(ClusterMetadata.current())) + return; } private void throwTimeout() diff --git a/src/java/org/apache/cassandra/service/ActiveRepairService.java b/src/java/org/apache/cassandra/service/ActiveRepairService.java index 3c709950f0..cc69368248 100644 --- a/src/java/org/apache/cassandra/service/ActiveRepairService.java +++ b/src/java/org/apache/cassandra/service/ActiveRepairService.java @@ -19,7 +19,16 @@ package org.apache.cassandra.service; import java.io.IOException; import java.net.UnknownHostException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledFuture; @@ -30,6 +39,7 @@ import javax.management.openmbean.CompositeData; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; @@ -41,33 +51,18 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; -import org.apache.cassandra.concurrent.ExecutorPlus; -import org.apache.cassandra.config.Config; -import org.apache.cassandra.config.DurationSpec; -import org.apache.cassandra.repair.SharedContext; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsByRange; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.utils.ExecutorUtils; -import org.apache.cassandra.repair.state.CoordinatorState; -import org.apache.cassandra.repair.state.ParticipateState; -import org.apache.cassandra.repair.state.ValidationState; -import org.apache.cassandra.utils.Simulate; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.streaming.PreviewKind; -import org.apache.cassandra.utils.TimeUUID; -import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.DurationSpec; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.gms.ApplicationState; import org.apache.cassandra.gms.EndpointState; @@ -75,25 +70,25 @@ import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.IEndpointStateChangeSubscriber; import org.apache.cassandra.gms.IFailureDetectionEventListener; import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.locator.EndpointsByRange; +import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.metrics.RepairMetrics; +import org.apache.cassandra.net.Message; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; -import org.apache.cassandra.net.Message; import org.apache.cassandra.repair.CommonRange; import org.apache.cassandra.repair.NoSuchRepairSessionException; -import org.apache.cassandra.service.paxos.PaxosRepair; -import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; import org.apache.cassandra.repair.RepairJobDesc; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.repair.RepairSession; +import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.repair.consistent.CoordinatorSessions; import org.apache.cassandra.repair.consistent.LocalSessions; +import org.apache.cassandra.repair.consistent.RepairedState; import org.apache.cassandra.repair.consistent.admin.CleanupSummary; import org.apache.cassandra.repair.consistent.admin.PendingStats; import org.apache.cassandra.repair.consistent.admin.RepairStats; -import org.apache.cassandra.repair.consistent.RepairedState; import org.apache.cassandra.repair.consistent.admin.SchemaArgsParser; import org.apache.cassandra.repair.messages.CleanupMessage; import org.apache.cassandra.repair.messages.PrepareMessage; @@ -101,9 +96,21 @@ import org.apache.cassandra.repair.messages.RepairMessage; import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.repair.messages.SyncResponse; import org.apache.cassandra.repair.messages.ValidationResponse; +import org.apache.cassandra.repair.state.CoordinatorState; +import org.apache.cassandra.repair.state.ParticipateState; +import org.apache.cassandra.repair.state.ValidationState; import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.paxos.PaxosRepair; +import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.MerkleTrees; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.Simulate; +import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.ImmediateFuture; @@ -554,10 +561,10 @@ public class ActiveRepairService implements IEndpointStateChangeSubscriber, IFai // same as withoutSelf(), but done this way for testing EndpointsForRange neighbors = replicaSets.get(rangeSuperSet).filter(r -> !ctx.broadcastAddressAndPort().equals(r.endpoint())); + ClusterMetadata metadata = ClusterMetadata.current(); if (dataCenters != null && !dataCenters.isEmpty()) { - TokenMetadata.Topology topology = ss.getTokenMetadata().cloneOnlyTokenMap().getTopology(); - Multimap dcEndpointsMap = topology.getDatacenterEndpoints(); + Multimap dcEndpointsMap = metadata.directory.allDatacenterEndpoints(); Iterable dcEndpoints = concat(transform(dataCenters, dcEndpointsMap::get)); return neighbors.select(dcEndpoints, true); } @@ -810,9 +817,9 @@ public class ActiveRepairService implements IEndpointStateChangeSubscriber, IFai } /** - * We assume when calling this method that a parent session for the provided identifier - * exists, and that session is still in progress. When it doesn't, that should mean either - * {@link #abort(Predicate, String)} or {@link #failRepair(TimeUUID, String)} have removed it. + * We assume when calling this method that a parent session for the provided identifier + * exists, and that session is still in progress. When it doesn't, that should mean either + * {@link #abort(Predicate, String)} or failRepair(UUID, String) have removed it. * * @param parentSessionId an identifier for an active parent repair session * @return the {@link ParentRepairSession} associated with the provided identifier @@ -1075,30 +1082,52 @@ public class ActiveRepairService implements IEndpointStateChangeSubscriber, IFai } public Future repairPaxosForTopologyChange(String ksName, Collection> ranges, String reason) + { + List>> work = repairPaxosForTopologyChangeAsync(ksName,ranges, reason); + List> futures = new ArrayList<>(); + + for (Supplier> futureSupplier : work) + futures.add(futureSupplier.get()); + + return FutureCombiner.allOf(futures); + } + + public List>> repairPaxosForTopologyChangeAsync(String ksName, Collection> ranges, String reason) { if (!paxosRepairEnabled()) { logger.warn("Not running paxos repair for topology change because paxos repair has been disabled"); - return ImmediateFuture.success(null); + return Arrays.asList(() -> ImmediateFuture.success(null)); } if (ranges.isEmpty()) { logger.warn("Not running paxos repair for topology change because there are no ranges to repair"); - return ImmediateFuture.success(null); + return Arrays.asList(() -> ImmediateFuture.success(null)); } - List tables = Lists.newArrayList(Schema.instance.getKeyspaceMetadata(ksName).tables); - List> futures = new ArrayList<>(ranges.size() * tables.size()); + ClusterMetadata metadata = ClusterMetadata.current(); + List tables = Lists.newArrayList(metadata.schema.getKeyspaces().getNullable(ksName).tables); + List>> futures = new ArrayList<>(ranges.size() * tables.size()); Keyspace keyspace = Keyspace.open(ksName); - AbstractReplicationStrategy replication = keyspace.getReplicationStrategy(); + for (Range range: ranges) { for (TableMetadata table : tables) { - Set endpoints = replication.getNaturalReplicas(range.right).filter(FailureDetector.isReplicaAlive).endpoints(); + Set endpoints = metadata.placements + .get(keyspace.getMetadata().params.replication) + .reads + .forRange(range) + .get() + .filter(FailureDetector.isReplicaAlive).endpoints(); if (!PaxosRepair.hasSufficientLiveNodesForTopologyChange(keyspace, range, endpoints)) { - Set downEndpoints = replication.getNaturalReplicas(range.right).filter(e -> !endpoints.contains(e)).endpoints(); + Set downEndpoints = metadata.placements + .get(keyspace.getMetadata().params.replication) + .reads + .forRange(range) + .get() + .filter(e -> !endpoints.contains(e.endpoint())).endpoints(); downEndpoints.removeAll(endpoints); throw new RuntimeException(String.format("Insufficient live nodes to repair paxos for %s in %s for %s.\n" + @@ -1113,20 +1142,19 @@ public class ActiveRepairService implements IEndpointStateChangeSubscriber, IFai "Skipping this check can lead to paxos correctness issues", range, ksName, reason, downEndpoints, SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE.getKey(), SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE_KEYSPACES.getKey())); } - EndpointsForToken pending = StorageService.instance.getTokenMetadata().pendingEndpointsForToken(range.right, ksName); - if (pending.size() > 1 && !PAXOS_REPAIR_ALLOW_MULTIPLE_PENDING_UNSAFE.getBoolean()) + // todo: can probably be removed with TrM + if (ClusterMetadata.current().hasPendingRangesFor(keyspace.getMetadata(), range.right) && PAXOS_REPAIR_ALLOW_MULTIPLE_PENDING_UNSAFE.getBoolean()) { - throw new RuntimeException(String.format("Cannot begin paxos auto repair for %s in %s.%s, multiple pending endpoints exist for range (%s). " + + throw new RuntimeException(String.format("Cannot begin paxos auto repair for %s in %s.%s, multiple pending endpoints exist for range (metadata = %s). " + "Set -D%s=true to skip this check", - range, table.keyspace, table.name, pending, PAXOS_REPAIR_ALLOW_MULTIPLE_PENDING_UNSAFE.getKey())); + range, table.keyspace, table.name, ClusterMetadata.current(), PAXOS_REPAIR_ALLOW_MULTIPLE_PENDING_UNSAFE.getKey())); } - Future future = PaxosCleanup.cleanup(endpoints, table, Collections.singleton(range), false, repairCommandExecutor()); - futures.add(future); + futures.add(() -> PaxosCleanup.cleanup(endpoints, table, Collections.singleton(range), false, repairCommandExecutor())); } } - return FutureCombiner.allOf(futures); + return futures; } public int getPaxosRepairParallelism() @@ -1195,4 +1223,4 @@ public class ActiveRepairService implements IEndpointStateChangeSubscriber, IFai } return null; } -} +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java index 1e1ac30d11..e0bc642a73 100644 --- a/src/java/org/apache/cassandra/service/CassandraDaemon.java +++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java @@ -25,27 +25,29 @@ import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; - import javax.management.ObjectName; import javax.management.StandardMBean; import javax.management.remote.JMXConnectorServer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistryListener; import com.codahale.metrics.SharedMetricRegistries; - import org.apache.cassandra.audit.AuditLogManager; import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.db.ColumnFamilyStore; @@ -59,7 +61,6 @@ import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.StartupException; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.locator.InetAddressAndPort; @@ -68,10 +69,14 @@ import org.apache.cassandra.metrics.DefaultNameFactory; import org.apache.cassandra.net.StartupClusterConnectivityChecker; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.security.ThreadAwareSecurityManager; -import org.apache.cassandra.streaming.StreamManager; import org.apache.cassandra.service.paxos.PaxosState; +import org.apache.cassandra.streaming.StreamManager; +import org.apache.cassandra.tcm.CMSOperations; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Startup; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.JMXServerUtils; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -105,6 +110,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.START_NATI public class CassandraDaemon { public static final String MBEAN_NAME = "org.apache.cassandra.db:type=NativeAccess"; + public static boolean SKIP_GC_INSPECTOR = CassandraRelevantProperties.SKIP_GC_INSPECTOR.getBoolean(); private static final Logger logger; @@ -250,10 +256,30 @@ public class CassandraDaemon NativeLibrary.tryMlockall(); + DatabaseDescriptor.createAllDirectories(); + Keyspace.setInitialized(); CommitLog.instance.start(); - runStartupChecks(); + + try + { + disableAutoCompaction(Schema.instance.localKeyspaces().names()); + Startup.initialize(DatabaseDescriptor.getSeeds()); + disableAutoCompaction(Schema.instance.distributedKeyspaces().names()); + CMSOperations.initJmx(); + } + catch (InterruptedException | ExecutionException | IOException e) + { + throw new AssertionError("Can't initialize cluster metadata service"); + } + catch (StartupException e) + { + exitOrFail(e.returnCode, e.getMessage(), e.getCause()); + } + + QueryProcessor.registerStatementInvalidatingListener(); + try { SystemKeyspace.snapshotOnVersionChange(); @@ -270,49 +296,8 @@ public class CassandraDaemon Thread.setDefaultUncaughtExceptionHandler(JVMStabilityInspector::uncaughtException); SystemKeyspaceMigrator41.migrate(); - - // Populate token metadata before flushing, for token-aware sstable partitioning (#6696) - StorageService.instance.populateTokenMetadata(); - - try - { - // load schema from disk - Schema.instance.loadFromDisk(); - } - catch (Exception e) - { - logger.error("Error while loading schema: ", e); - throw e; - } - setupVirtualKeyspaces(); - try - { - scrubDataDirectories(); - } - catch (StartupException e) - { - exitOrFail(e.returnCode, e.getMessage(), e.getCause()); - } - - Keyspace.setInitialized(); - - // initialize keyspaces - for (String keyspaceName : Schema.instance.getKeyspaces()) - { - if (logger.isDebugEnabled()) - logger.debug("opening keyspace {}", keyspaceName); - // disable auto compaction until gossip settles since disk boundaries may be affected by ring layout - for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores()) - { - for (ColumnFamilyStore store : cfs.concatWithIndexes()) - { - store.disableAutoCompaction(); - } - } - } - try { loadRowAndKeyCacheAsync().get(); @@ -323,20 +308,25 @@ public class CassandraDaemon logger.warn("Error loading key or row cache", t); } - try + if (!SKIP_GC_INSPECTOR) { - GCInspector.register(); - } - catch (Throwable t) - { - JVMStabilityInspector.inspectThrowable(t); - logger.warn("Unable to start GCInspector (currently only supported on the Sun JVM)"); + try + { + GCInspector.register(); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.warn("Unable to start GCInspector (currently only supported on the Sun JVM)"); + } } // Replay any CommitLogSegments found on disk PaxosState.initializeTrackers(); // replay the log if necessary + // TODO samt - when restarting a previously running instance, this needs to happen after reconstructing schema + // from the cluster metadata log or all mutations will throw IncompatibleSchemaException on deserialisation try { CommitLog.instance.recoverSegmentsOnDisk(); @@ -346,9 +336,6 @@ public class CassandraDaemon throw new RuntimeException(e); } - // Re-populate token metadata after commit log recover (new peers might be loaded onto system keyspace #10293) - StorageService.instance.populateTokenMetadata(); - try { PaxosState.maybeRebuildUncommittedState(); @@ -359,7 +346,7 @@ public class CassandraDaemon } // Clean up system.size_estimates entries left lying around from missed keyspace drops (CASSANDRA-14905) - StorageService.instance.cleanupSizeEstimates(); + SystemKeyspace.clearAllEstimates(); // schedule periodic dumps of table size estimates into SystemKeyspace.SIZE_ESTIMATES_CF // set cassandra.size_recorder_interval to 0 to disable @@ -385,6 +372,9 @@ public class CassandraDaemon exitOrFail(1, "Fatal configuration error", e); } + ScheduledExecutors.optionalTasks.execute(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + + // TODO: (TM/alexp), this can be made time-dependent // Because we are writing to the system_distributed keyspace, this should happen after that is created, which // happens in StorageService.instance.initServer() Runnable viewRebuild = () -> { @@ -397,33 +387,14 @@ public class CassandraDaemon ScheduledExecutors.optionalTasks.schedule(viewRebuild, StorageService.RING_DELAY_MILLIS, TimeUnit.MILLISECONDS); - if (!FBUtilities.getBroadcastAddressAndPort().equals(InetAddressAndPort.getLoopbackAddress())) - Gossiper.waitToSettle(); + // TODO: (TM/alexp), we do not need to wait for gossip settlement anymore +// if (!FBUtilities.getBroadcastAddressAndPort().equals(InetAddressAndPort.getLoopbackAddress())) +// Gossiper.waitToSettle(); - StorageService.instance.doAuthSetup(false); + StorageService.instance.doAuthSetup(); - // re-enable auto-compaction after gossip is settled, so correct disk boundaries are used - for (Keyspace keyspace : Keyspace.all()) - { - for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) - { - for (final ColumnFamilyStore store : cfs.concatWithIndexes()) - { - store.reload(); //reload CFs in case there was a change of disk boundaries - if (store.getCompactionStrategyManager().shouldBeEnabled()) - { - if (DatabaseDescriptor.getAutocompactionOnStartupEnabled()) - { - store.enableAutoCompaction(); - } - else - { - logger.info("Not enabling compaction for {}.{}; autocompaction_on_startup_enabled is set to false", store.getKeyspaceName(), store.name); - } - } - } - } - } + // re-enable auto-compaction after replay, so correct disk boundaries are used + enableAutoCompaction(Schema.instance.getKeyspaces()); AuditLogManager.instance.initialize(); @@ -530,6 +501,49 @@ public class CassandraDaemon } } + public static void disableAutoCompaction(Collection keyspaces) + { + for (String keyspaceName : keyspaces) + { + if (logger.isDebugEnabled()) + logger.debug("opening keyspace {}", keyspaceName); + // disable auto compaction until gossip settles since disk boundaries may be affected by ring layout + for (ColumnFamilyStore cfs : Keyspace.open(keyspaceName).getColumnFamilyStores()) + { + for (ColumnFamilyStore store : cfs.concatWithIndexes()) + { + store.disableAutoCompaction(); + } + } + } + } + + public static void enableAutoCompaction(Collection keyspaces) + { + for (String ksNme : keyspaces) + { + Keyspace keyspace = Keyspace.open(ksNme); + for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores()) + { + for (final ColumnFamilyStore store : cfs.concatWithIndexes()) + { + store.reload(store.metadata()); //reload CFs in case there was a change of disk boundaries + if (store.getCompactionStrategyManager().shouldBeEnabled()) + { + if (DatabaseDescriptor.getAutocompactionOnStartupEnabled()) + { + store.enableAutoCompaction(); + } + else + { + logger.info("Not enabling compaction for {}.{}; autocompaction_on_startup_enabled is set to false", store.getKeyspaceName(), store.name); + } + } + } + } + } + } + public void setupVirtualKeyspaces() { VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance); @@ -542,22 +556,6 @@ public class CassandraDaemon .ifPresent(appender -> ((VirtualTableAppender) appender).flushBuffer()); } - public void scrubDataDirectories() throws StartupException - { - // clean up debris in the rest of the keyspaces - for (String keyspaceName : Schema.instance.getKeyspaces()) - { - // Skip system as we've already cleaned it - if (keyspaceName.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME)) - continue; - - for (TableMetadata cfm : Schema.instance.getTablesAndViews(keyspaceName)) - { - ColumnFamilyStore.scrubDataDirectories(cfm); - } - } - } - public synchronized void initializeClientTransports() { // Native transport @@ -643,7 +641,8 @@ public class CassandraDaemon { StartupClusterConnectivityChecker connectivityChecker = StartupClusterConnectivityChecker.create(DatabaseDescriptor.getBlockForPeersTimeoutInSeconds(), DatabaseDescriptor.getBlockForPeersInRemoteDatacenters()); - connectivityChecker.execute(Gossiper.instance.getEndpoints(), DatabaseDescriptor.getEndpointSnitch()::getDatacenter); + Set peers = new HashSet<>(ClusterMetadata.current().directory.allJoinedEndpoints()); + connectivityChecker.execute(peers, DatabaseDescriptor.getEndpointSnitch()::getDatacenter); // check to see if transports may start else return without starting. This is needed when in survey mode or // when bootstrap has not completed. @@ -665,7 +664,10 @@ public class CassandraDaemon { String nativeFlag = START_NATIVE_TRANSPORT.getString(); if (START_NATIVE_TRANSPORT.getBoolean() || (nativeFlag == null && DatabaseDescriptor.startNativeTransport())) + { startNativeTransport(); + StorageService.instance.setRpcReady(true); + } else logger.info("Not starting native transport as requested. Use JMX (StorageService->startNativeTransport()) or nodetool (enablebinary) to start it"); } @@ -714,7 +716,7 @@ public class CassandraDaemon /** * A convenience method to initialize and start the daemon in one shot. */ - public void activate() + public void activate(boolean closeStdOutErr) { // Do not put any references to DatabaseDescriptor above the forceStaticInitialization call. try @@ -732,7 +734,7 @@ public class CassandraDaemon new File(pidFile).deleteOnExit(); } - if (CASSANDRA_FOREGROUND.getString() == null) + if (closeStdOutErr) { System.out.close(); System.err.close(); @@ -781,14 +783,17 @@ public class CassandraDaemon public void validateTransportsCanStart() { - // We only start transports if bootstrap has completed and we're not in survey mode, OR if we are in - // survey mode and streaming has completed but we're not using auth. + ClusterMetadata metadata = ClusterMetadata.current(); + MultiStepOperation startupSequence = metadata.inProgressSequences.get(metadata.myNodeId()); + + // We only start transports if bootstrap has completed, and we're not in survey mode, OR if we are in + // survey mode and streaming has completed, but we're not using auth. // OR if we have not joined the ring yet. - if (StorageService.instance.hasJoined()) + if (startupSequence != null) { if (StorageService.instance.isSurveyMode()) { - if (StorageService.instance.isBootstrapMode() || DatabaseDescriptor.getAuthenticator().requireAuthentication()) + if (!StorageService.instance.readyToFinishJoiningRing() || DatabaseDescriptor.getAuthenticator().requireAuthentication()) { throw new IllegalStateException("Not starting client transports in write_survey mode as it's bootstrapping or " + "auth is enabled"); @@ -796,11 +801,24 @@ public class CassandraDaemon } else { - if (!SystemKeyspace.bootstrapComplete()) - { - throw new IllegalStateException("Node is not yet bootstrapped completely. Use nodetool to check bootstrap" + - " state and resume. For more, see `nodetool help bootstrap`"); - } + throw new IllegalStateException("Node is not yet bootstrapped completely"); + } + } + else + { + // Bootstrap with same address is an edge-case here, since we rely on HIBERNATE to prevent writes + // toward the bootstrapping replacement, so there's no startup sequence involved. + if (StorageService.instance.isReplacingSameAddress() && StorageService.instance.isSurveyMode()) + return; + + // This node has not joined the ring (i.e. it was started with -Dcassandra.join_ring=false) + if (StorageService.instance.isStarting()) + return; + + if (!SystemKeyspace.bootstrapComplete()) + { + throw new IllegalStateException("Node is not yet bootstrapped completely. Use nodetool to check bootstrap" + + " state and resume. For more, see `nodetool help bootstrap`"); } } } @@ -858,7 +876,7 @@ public class CassandraDaemon public static void main(String[] args) { - instance.activate(); + instance.activate(CASSANDRA_FOREGROUND.getString() == null); } public void clearConnectionHistory() diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java index d9a0dbbc9e..10940c8b87 100644 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@ -140,6 +140,27 @@ public class ClientState // is unrealistic expectation, doing it node-wise is easy). private static final AtomicLong lastTimestampMicros = new AtomicLong(0); + private boolean applyGuardrails = true; + /** + * Provides an additional control on the checking of guardrails. When executing SchemaTransformations in the + * metadata log follower or when committing on a CMS member, we don't want guardrails to fire warnings. + * @see org.apache.cassandra.schema.SchemaTransformation#enterExecution() + **/ + public void pauseGuardrails() + { + applyGuardrails = false; + } + + public void resumeGuardrails() + { + applyGuardrails = true; + } + + public boolean applyGuardrails() + { + return applyGuardrails; + } + @VisibleForTesting public static void resetLastTimestamp(long nowMillis) { diff --git a/src/java/org/apache/cassandra/service/ClientWarn.java b/src/java/org/apache/cassandra/service/ClientWarn.java index 1769425c63..56aa421b19 100644 --- a/src/java/org/apache/cassandra/service/ClientWarn.java +++ b/src/java/org/apache/cassandra/service/ClientWarn.java @@ -55,6 +55,25 @@ public class ClientWarn extends ExecutorLocals.Impl set(new State()); } + /** + * Provides an additional control on capturing warnings. When executing SchemaTransformations in the + * metadata log follower or when committing on a CMS member, we don't want these to be triggered. + * @see org.apache.cassandra.schema.SchemaTransformation#enterExecution() + **/ + public void pauseCapture() + { + State state = get(); + if (state != null) + state.collecting = false; + } + + public void resumeCapture() + { + State state = get(); + if (state != null) + state.collecting = true; + } + public List getWarnings() { State state = get(); @@ -70,11 +89,12 @@ public class ClientWarn extends ExecutorLocals.Impl public static class State { + private boolean collecting = true; private final List warnings = new ArrayList<>(); private void add(String warning) { - if (warnings.size() < FBUtilities.MAX_UNSIGNED_SHORT) + if (collecting && warnings.size() < FBUtilities.MAX_UNSIGNED_SHORT) warnings.add(maybeTruncate(warning)); } diff --git a/src/java/org/apache/cassandra/service/EchoVerbHandler.java b/src/java/org/apache/cassandra/service/EchoVerbHandler.java index 228808dd65..7a6c1e1b7c 100644 --- a/src/java/org/apache/cassandra/service/EchoVerbHandler.java +++ b/src/java/org/apache/cassandra/service/EchoVerbHandler.java @@ -19,6 +19,7 @@ package org.apache.cassandra.service; * under the License. * */ +import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; @@ -36,7 +37,7 @@ public class EchoVerbHandler implements IVerbHandler public void doVerb(Message message) { // only respond if we are not shutdown - if (!StorageService.instance.isShutdown()) + if (!StorageService.instance.isShutdown() && !Gossiper.instance.shutdownAnnounced.get()) { logger.trace("Sending ECHO_RSP to {}", message.from()); MessagingService.instance().send(message.emptyResponse(), message.from()); diff --git a/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java b/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java deleted file mode 100644 index cd096a888e..0000000000 --- a/src/java/org/apache/cassandra/service/PendingRangeCalculatorService.java +++ /dev/null @@ -1,98 +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; - -import java.util.Collection; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.SequentialExecutorPlus; -import org.apache.cassandra.concurrent.SequentialExecutorPlus.AtLeastOnceTrigger; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.utils.ExecutorUtils; - -import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; - -public class PendingRangeCalculatorService -{ - public static final PendingRangeCalculatorService instance = new PendingRangeCalculatorService(); - - private static final Logger logger = LoggerFactory.getLogger(PendingRangeCalculatorService.class); - - // the executor will only run a single range calculation at a time while keeping at most one task queued in order - // to trigger an update only after the most recent state change and not for each update individually - private final SequentialExecutorPlus executor = executorFactory() - .withJmxInternal() - .configureSequential("PendingRangeCalculator") - .withRejectedExecutionHandler((r, e) -> {}) // silently handle rejected tasks, this::update takes care of bookkeeping - .build(); - - private final AtLeastOnceTrigger update = executor.atLeastOnceTrigger(() -> { - PendingRangeCalculatorServiceDiagnostics.taskStarted(1); - long start = currentTimeMillis(); - Collection keyspaces = Schema.instance.distributedKeyspaces().names(); - for (String keyspaceName : keyspaces) - calculatePendingRanges(Keyspace.open(keyspaceName).getReplicationStrategy(), keyspaceName); - if (logger.isTraceEnabled()) - logger.trace("Finished PendingRangeTask for {} keyspaces in {}ms", keyspaces.size(), currentTimeMillis() - start); - PendingRangeCalculatorServiceDiagnostics.taskFinished(); - }); - - public PendingRangeCalculatorService() - { - } - - public void update() - { - boolean success = update.trigger(); - if (!success) PendingRangeCalculatorServiceDiagnostics.taskRejected(1); - else PendingRangeCalculatorServiceDiagnostics.taskCountChanged(1); - } - - public void blockUntilFinished() - { - update.sync(); - } - - - public void executeWhenFinished(Runnable runnable) - { - update.runAfter(runnable); - } - - // public & static for testing purposes - public static void calculatePendingRanges(AbstractReplicationStrategy strategy, String keyspaceName) - { - StorageService.instance.getTokenMetadata().calculatePendingRanges(strategy, keyspaceName); - } - - @VisibleForTesting - public void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException - { - ExecutorUtils.shutdownNowAndWait(timeout, unit, executor); - } - -} diff --git a/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceDiagnostics.java b/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceDiagnostics.java deleted file mode 100644 index 2b677d1fe6..0000000000 --- a/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceDiagnostics.java +++ /dev/null @@ -1,66 +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; - -import org.apache.cassandra.diag.DiagnosticEventService; -import org.apache.cassandra.service.PendingRangeCalculatorServiceEvent.PendingRangeCalculatorServiceEventType; - -/** - * Utility methods for diagnostic events related to {@link PendingRangeCalculatorService}. - */ -final class PendingRangeCalculatorServiceDiagnostics -{ - private static final DiagnosticEventService service = DiagnosticEventService.instance(); - - private PendingRangeCalculatorServiceDiagnostics() - { - } - - static void taskStarted(int taskCount) - { - if (isEnabled(PendingRangeCalculatorServiceEventType.TASK_STARTED)) - service.publish(new PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType.TASK_STARTED, - taskCount)); - } - - static void taskFinished() - { - if (isEnabled(PendingRangeCalculatorServiceEventType.TASK_FINISHED_SUCCESSFULLY)) - service.publish(new PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType.TASK_FINISHED_SUCCESSFULLY)); - } - - static void taskRejected(int taskCount) - { - if (isEnabled(PendingRangeCalculatorServiceEventType.TASK_EXECUTION_REJECTED)) - service.publish(new PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType.TASK_EXECUTION_REJECTED, - taskCount)); - } - - static void taskCountChanged(int taskCount) - { - if (isEnabled(PendingRangeCalculatorServiceEventType.TASK_COUNT_CHANGED)) - service.publish(new PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType.TASK_COUNT_CHANGED, - taskCount)); - } - - private static boolean isEnabled(PendingRangeCalculatorServiceEventType type) - { - return service.isEnabled(PendingRangeCalculatorServiceEvent.class, type); - } -} diff --git a/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceEvent.java b/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceEvent.java deleted file mode 100644 index f25517347c..0000000000 --- a/src/java/org/apache/cassandra/service/PendingRangeCalculatorServiceEvent.java +++ /dev/null @@ -1,75 +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; - -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.apache.cassandra.diag.DiagnosticEvent; - -/** - * Events related to {@link PendingRangeCalculatorService}. - */ -final class PendingRangeCalculatorServiceEvent extends DiagnosticEvent -{ - private final PendingRangeCalculatorServiceEventType type; - private final int taskCount; - - public enum PendingRangeCalculatorServiceEventType - { - TASK_STARTED, - TASK_FINISHED_SUCCESSFULLY, - TASK_EXECUTION_REJECTED, - TASK_COUNT_CHANGED - } - - PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType type) - { - this(type, -1); - } - - PendingRangeCalculatorServiceEvent(PendingRangeCalculatorServiceEventType type, - int taskCount) - { - this.type = type; - this.taskCount = taskCount; - } - - public int getTaskCount() - { - return taskCount; - } - - public PendingRangeCalculatorServiceEventType getType() - { - return type; - } - - public Map toMap() - { - // be extra defensive against nulls and bugs - if (taskCount < 0) - return Collections.emptyMap(); - HashMap ret = new HashMap<>(); - ret.put("taskCount", taskCount); - return ret; - } -} diff --git a/src/java/org/apache/cassandra/service/RangeRelocator.java b/src/java/org/apache/cassandra/service/RangeRelocator.java deleted file mode 100644 index b63c105bd2..0000000000 --- a/src/java/org/apache/cassandra/service/RangeRelocator.java +++ /dev/null @@ -1,323 +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; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Future; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Multimap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.RangeStreamer; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.FailureDetector; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsByReplica; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.RangesAtEndpoint; -import org.apache.cassandra.locator.RangesByEndpoint; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.streaming.StreamOperation; -import org.apache.cassandra.streaming.StreamPlan; -import org.apache.cassandra.streaming.StreamState; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; - -@VisibleForTesting -public class RangeRelocator -{ - private static final Logger logger = LoggerFactory.getLogger(StorageService.class); - - private final StreamPlan streamPlan = new StreamPlan(StreamOperation.RELOCATION); - private final InetAddressAndPort localAddress = FBUtilities.getBroadcastAddressAndPort(); - private final TokenMetadata tokenMetaCloneAllSettled; - // clone to avoid concurrent modification in calculateNaturalReplicas - private final TokenMetadata tokenMetaClone; - private final Collection tokens; - private final List keyspaceNames; - - - RangeRelocator(Collection tokens, List keyspaceNames, TokenMetadata tmd) - { - this.tokens = tokens; - this.keyspaceNames = keyspaceNames; - this.tokenMetaCloneAllSettled = tmd.cloneAfterAllSettled(); - // clone to avoid concurrent modification in calculateNaturalReplicas - this.tokenMetaClone = tmd.cloneOnlyTokenMap(); - } - - @VisibleForTesting - public RangeRelocator() - { - this.tokens = null; - this.keyspaceNames = null; - this.tokenMetaCloneAllSettled = null; - this.tokenMetaClone = null; - } - - /** - * Wrapper that supplies accessors to the real implementations of the various dependencies for this method - */ - private static Multimap calculateRangesToFetchWithPreferredEndpoints(RangesAtEndpoint fetchRanges, - AbstractReplicationStrategy strategy, - String keyspace, - TokenMetadata tmdBefore, - TokenMetadata tmdAfter) - { - EndpointsByReplica preferredEndpoints = - RangeStreamer.calculateRangesToFetchWithPreferredEndpoints(DatabaseDescriptor.getEndpointSnitch()::sortedByProximity, - strategy, - fetchRanges, - StorageService.useStrictConsistency, - tmdBefore, - tmdAfter, - keyspace, - Arrays.asList(new RangeStreamer.FailureDetectorSourceFilter(FailureDetector.instance), - new RangeStreamer.ExcludeLocalNodeFilter())); - return RangeStreamer.convertPreferredEndpointsToWorkMap(preferredEndpoints); - } - - /** - * calculating endpoints to stream current ranges to if needed - * in some situations node will handle current ranges as part of the new ranges - **/ - public static RangesByEndpoint calculateRangesToStreamWithEndpoints(RangesAtEndpoint streamRanges, - AbstractReplicationStrategy strat, - TokenMetadata tmdBefore, - TokenMetadata tmdAfter) - { - RangesByEndpoint.Builder endpointRanges = new RangesByEndpoint.Builder(); - for (Replica toStream : streamRanges) - { - //If the range we are sending is full only send it to the new full replica - //There will also be a new transient replica we need to send the data to, but not - //the repaired data - EndpointsForRange oldEndpoints = strat.calculateNaturalReplicas(toStream.range().right, tmdBefore); - EndpointsForRange newEndpoints = strat.calculateNaturalReplicas(toStream.range().right, tmdAfter); - logger.debug("Need to stream {}, current endpoints {}, new endpoints {}", toStream, oldEndpoints, newEndpoints); - - for (Replica newEndpoint : newEndpoints) - { - Replica oldEndpoint = oldEndpoints.byEndpoint().get(newEndpoint.endpoint()); - - // Nothing to do - if (newEndpoint.equals(oldEndpoint)) - continue; - - // Completely new range for this endpoint - if (oldEndpoint == null) - { - if (toStream.isTransient() && newEndpoint.isFull()) - throw new AssertionError(String.format("Need to stream %s, but only have %s which is transient and not full", newEndpoint, toStream)); - - for (Range intersection : newEndpoint.range().intersectionWith(toStream.range())) - { - endpointRanges.put(newEndpoint.endpoint(), newEndpoint.decorateSubrange(intersection)); - } - } - else - { - Set> subsToStream = Collections.singleton(toStream.range()); - - //First subtract what we already have - if (oldEndpoint.isFull() == newEndpoint.isFull() || oldEndpoint.isFull()) - subsToStream = toStream.range().subtract(oldEndpoint.range()); - - //Now we only stream what is still replicated - subsToStream.stream() - .flatMap(range -> range.intersectionWith(newEndpoint.range()).stream()) - .forEach(tokenRange -> endpointRanges.put(newEndpoint.endpoint(), newEndpoint.decorateSubrange(tokenRange))); - } - } - } - return endpointRanges.build(); - } - - public void calculateToFromStreams() - { - logger.debug("Current tmd: {}, Updated tmd: {}", tokenMetaClone, tokenMetaCloneAllSettled); - - for (String keyspace : keyspaceNames) - { - // replication strategy of the current keyspace - AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); - - logger.info("Calculating ranges to stream and request for keyspace {}", keyspace); - //From what I have seen we only ever call this with a single token from StorageService.move(Token) - for (Token newToken : tokens) - { - Collection currentTokens = tokenMetaClone.getTokens(localAddress); - if (currentTokens.size() > 1 || currentTokens.isEmpty()) - { - throw new AssertionError("Unexpected current tokens: " + currentTokens); - } - - // calculated parts of the ranges to request/stream from/to nodes in the ring - Pair streamAndFetchOwnRanges; - - //In the single node token move there is nothing to do and Range subtraction is broken - //so it's easier to just identify this case up front. - if (tokenMetaClone.getTopology().getDatacenterEndpoints().get(DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter()).size() > 1) - { - // getting collection of the currently used ranges by this keyspace - RangesAtEndpoint currentReplicas = strategy.getAddressReplicas(localAddress); - - // collection of ranges which this node will serve after move to the new token - RangesAtEndpoint updatedReplicas = strategy.getPendingAddressRanges(tokenMetaClone, newToken, localAddress); - - streamAndFetchOwnRanges = calculateStreamAndFetchRanges(currentReplicas, updatedReplicas); - } - else - { - streamAndFetchOwnRanges = Pair.create(RangesAtEndpoint.empty(localAddress), RangesAtEndpoint.empty(localAddress)); - } - - RangesByEndpoint rangesToStream = calculateRangesToStreamWithEndpoints(streamAndFetchOwnRanges.left, strategy, tokenMetaClone, tokenMetaCloneAllSettled); - logger.info("Endpoint ranges to stream to " + rangesToStream); - - // stream ranges - for (InetAddressAndPort address : rangesToStream.keySet()) - { - logger.debug("Will stream range {} of keyspace {} to endpoint {}", rangesToStream.get(address), keyspace, address); - RangesAtEndpoint ranges = rangesToStream.get(address); - streamPlan.transferRanges(address, keyspace, ranges); - } - - Multimap rangesToFetch = calculateRangesToFetchWithPreferredEndpoints(streamAndFetchOwnRanges.right, strategy, keyspace, tokenMetaClone, tokenMetaCloneAllSettled); - - // stream requests - rangesToFetch.asMap().forEach((address, sourceAndOurReplicas) -> { - RangesAtEndpoint full = sourceAndOurReplicas.stream() - .filter(pair -> pair.remote.isFull()) - .map(pair -> pair.local) - .collect(RangesAtEndpoint.collector(localAddress)); - RangesAtEndpoint trans = sourceAndOurReplicas.stream() - .filter(pair -> pair.remote.isTransient()) - .map(pair -> pair.local) - .collect(RangesAtEndpoint.collector(localAddress)); - logger.debug("Will request range {} of keyspace {} from endpoint {}", rangesToFetch.get(address), keyspace, address); - streamPlan.requestRanges(address, keyspace, full, trans); - }); - - logger.debug("Keyspace {}: work map {}.", keyspace, rangesToFetch); - } - } - } - - /** - * Calculate pair of ranges to stream/fetch for given two range collections - * (current ranges for keyspace and ranges after move to new token) - * - * With transient replication the added wrinkle is that if a range transitions from full to transient then - * we need to stream the range despite the fact that we are retaining it as transient. Some replica - * somewhere needs to transition from transient to full and we will be the source. - * - * If the range is transient and is transitioning to full then always fetch even if the range was already transient - * since a transiently replicated obviously needs to fetch data to become full. - * - * This why there is a continue after checking for instersection because intersection is not sufficient reason - * to do the subtraction since we might need to stream/fetch data anyways. - * - * @param currentRanges collection of the ranges by current token - * @param updatedRanges collection of the ranges after token is changed - * @return pair of ranges to stream/fetch for given current and updated range collections - */ - public static Pair calculateStreamAndFetchRanges(RangesAtEndpoint currentRanges, RangesAtEndpoint updatedRanges) - { - RangesAtEndpoint.Builder toStream = RangesAtEndpoint.builder(currentRanges.endpoint()); - RangesAtEndpoint.Builder toFetch = RangesAtEndpoint.builder(currentRanges.endpoint()); - logger.debug("Calculating toStream"); - computeRanges(currentRanges, updatedRanges, toStream); - - logger.debug("Calculating toFetch"); - computeRanges(updatedRanges, currentRanges, toFetch); - - logger.debug("To stream {}", toStream); - logger.debug("To fetch {}", toFetch); - return Pair.create(toStream.build(), toFetch.build()); - } - - private static void computeRanges(RangesAtEndpoint srcRanges, RangesAtEndpoint dstRanges, RangesAtEndpoint.Builder ranges) - { - for (Replica src : srcRanges) - { - boolean intersect = false; - RangesAtEndpoint remainder = null; - for (Replica dst : dstRanges) - { - logger.debug("Comparing {} and {}", src, dst); - // Stream the full range if there's no intersection - if (!src.intersectsOnRange(dst)) - continue; - - // If we're transitioning from full to transient - if (src.isFull() && dst.isTransient()) - continue; - - if (remainder == null) - { - remainder = src.subtractIgnoreTransientStatus(dst.range()); - } - else - { - // Re-subtract ranges to avoid overstreaming in cases when the single range is split or merged - RangesAtEndpoint.Builder newRemainder = new RangesAtEndpoint.Builder(remainder.endpoint()); - for (Replica replica : remainder) - newRemainder.addAll(replica.subtractIgnoreTransientStatus(dst.range())); - remainder = newRemainder.build(); - } - intersect = true; - } - - if (!intersect) - { - assert remainder == null; - logger.debug(" Doesn't intersect adding {}", src); - ranges.add(src); // should stream whole old range - } - else - { - ranges.addAll(remainder); - logger.debug(" Intersects adding {}", remainder); - } - } - } - - public Future stream() - { - return streamPlan.execute(); - } - - public boolean streamsNeeded() - { - return !streamPlan.isEmpty(); - } -} diff --git a/src/java/org/apache/cassandra/service/Rebuild.java b/src/java/org/apache/cassandra/service/Rebuild.java new file mode 100644 index 0000000000..f089aded13 --- /dev/null +++ b/src/java/org/apache/cassandra/service/Rebuild.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.RangeStreamer; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; + +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; + +public class Rebuild +{ + private static final AtomicBoolean isRebuilding = new AtomicBoolean(); + + private static final Logger logger = LoggerFactory.getLogger(Rebuild.class); + + @VisibleForTesting + public static void unsafeResetRebuilding() + { + isRebuilding.set(false); + } + + public static void rebuild(String sourceDc, String keyspace, String tokens, String specificSources, boolean excludeLocalDatacenterNodes) + { + // check ongoing rebuild + if (!isRebuilding.compareAndSet(false, true)) + { + throw new IllegalStateException("Node is still rebuilding. Check nodetool netstats."); + } + + if (sourceDc != null) + { + if (sourceDc.equals(DatabaseDescriptor.getLocalDataCenter()) && excludeLocalDatacenterNodes) // fail if source DC is local and --exclude-local-dc is set + throw new IllegalArgumentException("Cannot set source data center to be local data center, when excludeLocalDataCenter flag is set"); + Set availableDCs = ClusterMetadata.current().directory.knownDatacenters(); + if (!availableDCs.contains(sourceDc)) + { + throw new IllegalArgumentException(String.format("Provided datacenter '%s' is not a valid datacenter, available datacenters are: %s", + sourceDc, String.join(",", availableDCs))); + } + } + + try + { + // check the arguments + if (keyspace == null && tokens != null) + { + throw new IllegalArgumentException("Cannot specify tokens without keyspace."); + } + + logger.info("rebuild from dc: {}, {}, {}", sourceDc == null ? "(any dc)" : sourceDc, + keyspace == null ? "(All keyspaces)" : keyspace, + tokens == null ? "(All tokens)" : tokens); + + StorageService.instance.repairPaxosForTopologyChange("rebuild"); + ClusterMetadata metadata = ClusterMetadata.current(); + MovementMap rebuildMovements = movementMap(metadata, keyspace, tokens); + logger.info("Rebuild movements: {}", rebuildMovements); + RangeStreamer streamer = new RangeStreamer(metadata, + StreamOperation.REBUILD, + false, // no strict consistency when rebuilding + DatabaseDescriptor.getEndpointSnitch(), + StorageService.instance.streamStateStore, + false, + DatabaseDescriptor.getStreamingConnectionsPerHost(), + rebuildMovements, + null); + if (sourceDc != null) + streamer.addSourceFilter(new RangeStreamer.SingleDatacenterFilter(DatabaseDescriptor.getEndpointSnitch(), sourceDc)); + + if (excludeLocalDatacenterNodes) + streamer.addSourceFilter(new RangeStreamer.ExcludeLocalDatacenterFilter(DatabaseDescriptor.getEndpointSnitch())); + + if (keyspace == null) + { + for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces().names()) + streamer.addKeyspaceToFetch(keyspaceName); + } + else if (tokens == null) + { + streamer.addKeyspaceToFetch(keyspace); + } + else + { + if (specificSources != null) + { + String[] stringHosts = specificSources.split(","); + Set sources = new HashSet<>(stringHosts.length); + for (String stringHost : stringHosts) + { + try + { + InetAddressAndPort endpoint = InetAddressAndPort.getByName(stringHost); + if (getBroadcastAddressAndPort().equals(endpoint)) + { + throw new IllegalArgumentException("This host was specified as a source for rebuilding. Sources for a rebuild can only be other nodes in the cluster."); + } + sources.add(endpoint); + } + catch (UnknownHostException ex) + { + throw new IllegalArgumentException("Unknown host specified " + stringHost, ex); + } + } + streamer.addSourceFilter(new RangeStreamer.AllowedSourcesFilter(sources)); + } + + streamer.addKeyspaceToFetch(keyspace); + } + + streamer.fetchAsync().get(); + } + catch (InterruptedException e) + { + throw new UncheckedInterruptedException(e); + } + catch (ExecutionException e) + { + // This is used exclusively through JMX, so log the full trace but only throw a simple RTE + logger.error("Error while rebuilding node", e.getCause()); + throw new RuntimeException("Error while rebuilding node: " + e.getCause().getMessage()); + } + finally + { + // rebuild is done (successfully or not) + isRebuilding.set(false); + } + } + + + private static RangesAtEndpoint rangesForRebuildWithTokens(String tokens, String keyspace) + { + Token.TokenFactory factory = StorageService.instance.getTokenFactory(); + List> ranges = new ArrayList<>(); + Pattern rangePattern = Pattern.compile("\\(\\s*(-?\\w+)\\s*,\\s*(-?\\w+)\\s*\\]"); + try (Scanner tokenScanner = new Scanner(tokens)) + { + while (tokenScanner.findInLine(rangePattern) != null) + { + MatchResult range = tokenScanner.match(); + Token startToken = factory.fromString(range.group(1)); + Token endToken = factory.fromString(range.group(2)); + logger.info("adding range: ({},{}]", startToken, endToken); + ranges.add(new Range<>(startToken, endToken)); + } + if (tokenScanner.hasNext()) + throw new IllegalArgumentException("Unexpected string: " + tokenScanner.next()); + } + + // Ensure all specified ranges are actually ranges owned by this host + RangesAtEndpoint localReplicas = StorageService.instance.getLocalReplicas(keyspace); + RangesAtEndpoint.Builder streamRanges = new RangesAtEndpoint.Builder(getBroadcastAddressAndPort(), ranges.size()); + for (Range specifiedRange : ranges) + { + boolean foundParentRange = false; + for (Replica localReplica : localReplicas) + { + if (localReplica.contains(specifiedRange)) + { + streamRanges.add(localReplica.decorateSubrange(specifiedRange)); + foundParentRange = true; + break; + } + } + if (!foundParentRange) + { + throw new IllegalArgumentException(String.format("The specified range %s is not a range that is owned by this node. Please ensure that all token ranges specified to be rebuilt belong to this node.", specifiedRange.toString())); + } + } + return streamRanges.build(); + } + + private static MovementMap movementMap(ClusterMetadata metadata, String keyspace, String tokens) + { + MovementMap.Builder movementMapBuilder = MovementMap.builder(); + DataPlacements placements = metadata.placements; + if (keyspace == null) + { + placements.forEach((params, placement) -> movementMapBuilder.put(params, addMovementsForParams(placement, null))); + } + else if (tokens == null) + { + ReplicationParams params = Keyspace.open(keyspace).getMetadata().params.replication; + movementMapBuilder.put(params, addMovementsForParams(placements.get(params), null)); + } + else + { + ReplicationParams params = Keyspace.open(keyspace).getMetadata().params.replication; + RangesAtEndpoint ranges = rangesForRebuildWithTokens(tokens, keyspace); + movementMapBuilder.put(params, addMovementsForParams(placements.get(params), ranges)); + } + return movementMapBuilder.build(); + } + + private static EndpointsByReplica addMovementsForParams(DataPlacement placement, RangesAtEndpoint ranges) + { + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + RangesAtEndpoint localReplicas = ranges != null ? ranges : placement.reads.byEndpoint().get(getBroadcastAddressAndPort()); + for (Replica localReplica : localReplicas) + { + placement.reads.forRange(localReplica.range().right).forEach(r -> { + if (!r.equals(localReplica)) + movements.put(localReplica, r); + }); + } + return movements.build(); + } +} diff --git a/src/java/org/apache/cassandra/service/StartupChecks.java b/src/java/org/apache/cassandra/service/StartupChecks.java index d04d559a3b..42e7798ade 100644 --- a/src/java/org/apache/cassandra/service/StartupChecks.java +++ b/src/java/org/apache/cassandra/service/StartupChecks.java @@ -105,8 +105,6 @@ public class StartupChecks // non-configurable check is always enabled for execution non_configurable_check, check_filesystem_ownership(true), - check_dc, - check_rack, check_data_resurrection(true); public final boolean disabledByDefault; @@ -676,36 +674,19 @@ public class StartupChecks @Override public void execute(StartupChecksOptions options) throws StartupException { - boolean enabled = options.isEnabled(getStartupCheckType()); - if (CassandraRelevantProperties.IGNORE_DC.isPresent()) + String storedDc = SystemKeyspace.getDatacenter(); + if (storedDc != null) { - logger.warn(String.format("Cassandra system property flag %s is deprecated and you should " + - "use startup check configuration in cassandra.yaml", - CassandraRelevantProperties.IGNORE_DC.getKey())); - enabled = !CassandraRelevantProperties.IGNORE_DC.getBoolean(); - } - if (enabled) - { - String storedDc = SystemKeyspace.getDatacenter(); - if (storedDc != null) + String currentDc = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); + if (!storedDc.equals(currentDc)) { - String currentDc = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); - if (!storedDc.equals(currentDc)) - { - String formatMessage = "Cannot start node if snitch's data center (%s) differs from previous data center (%s). " + - "Please fix the snitch configuration, decommission and rebootstrap this node or use the flag -Dcassandra.ignore_dc=true."; + String formatMessage = "Cannot start node if snitch's data center (%s) differs from previous data center (%s). " + + "Please fix the snitch configuration, decommission and rebootstrap this node"; - throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentDc, storedDc)); - } + throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentDc, storedDc)); } } } - - @Override - public StartupCheckType getStartupCheckType() - { - return StartupCheckType.check_dc; - } }; public static final StartupCheck checkRack = new StartupCheck() @@ -713,36 +694,19 @@ public class StartupChecks @Override public void execute(StartupChecksOptions options) throws StartupException { - boolean enabled = options.isEnabled(getStartupCheckType()); - if (CassandraRelevantProperties.IGNORE_RACK.isPresent()) + String storedRack = SystemKeyspace.getRack(); + if (storedRack != null) { - logger.warn(String.format("Cassandra system property flag %s is deprecated and you should " + - "use startup check configuration in cassandra.yaml", - CassandraRelevantProperties.IGNORE_RACK.getKey())); - enabled = !CassandraRelevantProperties.IGNORE_RACK.getBoolean(); - } - if (enabled) - { - String storedRack = SystemKeyspace.getRack(); - if (storedRack != null) + String currentRack = DatabaseDescriptor.getEndpointSnitch().getLocalRack(); + if (!storedRack.equals(currentRack)) { - String currentRack = DatabaseDescriptor.getEndpointSnitch().getLocalRack(); - if (!storedRack.equals(currentRack)) - { - String formatMessage = "Cannot start node if snitch's rack (%s) differs from previous rack (%s). " + - "Please fix the snitch configuration, decommission and rebootstrap this node or use the flag -Dcassandra.ignore_rack=true."; + String formatMessage = "Cannot start node if snitch's rack (%s) differs from previous rack (%s). " + + "Please fix the snitch configuration, decommission and rebootstrap this node"; - throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentRack, storedRack)); - } + throw new StartupException(StartupException.ERR_WRONG_CONFIG, String.format(formatMessage, currentRack, storedRack)); } } } - - @Override - public StartupCheckType getStartupCheckType() - { - return StartupCheckType.check_rack; - } }; public static final StartupCheck checkLegacyAuthTables = new StartupCheck() diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index e842ba7495..8c5127dd2b 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -43,7 +43,6 @@ import com.google.common.base.Preconditions; import com.google.common.cache.CacheLoader; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Uninterruptibles; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,14 +93,12 @@ import org.apache.cassandra.exceptions.RequestTimeoutException; import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; -import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.hints.Hint; import org.apache.cassandra.hints.HintsService; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.DynamicEndpointSnitch; import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaLayout; @@ -135,6 +132,9 @@ import org.apache.cassandra.service.reads.AbstractReadExecutor; import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.service.reads.range.RangeCommands; import org.apache.cassandra.service.reads.repair.ReadRepair; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.triggers.TriggerExecutor; import org.apache.cassandra.utils.Clock; @@ -147,12 +147,9 @@ import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import static com.google.common.collect.Iterables.concat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import static com.google.common.collect.Iterables.concat; -import static org.apache.commons.lang3.StringUtils.join; - import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; @@ -180,6 +177,7 @@ import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch; +import static org.apache.commons.lang3.StringUtils.join; public class StorageProxy implements StorageProxyMBean { @@ -317,10 +315,10 @@ public class StorageProxy implements StorageProxyMBean { denylistMetrics.incrementWritesRejected(); throw new InvalidRequestException(String.format("Unable to CAS write to denylisted partition [0x%s] in %s/%s", - key.toString(), keyspaceName, cfName)); + key, keyspaceName, cfName)); } - return Paxos.useV2() + return (Paxos.useV2() || keyspaceName.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) ? Paxos.cas(key, request, consistencyForPaxos, consistencyForCommit, clientState) : legacyCas(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit, clientState, nowInSeconds, queryStartNanoTime); } @@ -970,25 +968,27 @@ public class StorageProxy implements StorageProxyMBean private static void hintMutation(Mutation mutation) { + ClusterMetadata metadata = ClusterMetadata.current(); String keyspaceName = mutation.getKeyspaceName(); Token token = mutation.key().getToken(); // local writes can timeout, but cannot be dropped (see LocalMutationRunnable and CASSANDRA-6510), // so there is no need to hint or retry. - EndpointsForToken replicasToHint = ReplicaLayout.forTokenWriteLiveAndDown(Keyspace.open(keyspaceName), token) + EndpointsForToken replicasToHint = ReplicaLayout.forTokenWriteLiveAndDown(metadata, Keyspace.open(keyspaceName), token) .all() .filter(StorageProxy::shouldHint); submitHint(mutation, replicasToHint, null); } - public boolean appliesLocally(Mutation mutation) + public static boolean appliesLocally(Mutation mutation) { + ClusterMetadata metadata = ClusterMetadata.current(); String keyspaceName = mutation.getKeyspaceName(); Token token = mutation.key().getToken(); InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); - return ReplicaLayout.forTokenWriteLiveAndDown(Keyspace.open(keyspaceName), token) + return ReplicaLayout.forTokenWriteLiveAndDown(metadata, Keyspace.open(keyspaceName), token) .all().endpoints().contains(local); } @@ -1009,6 +1009,7 @@ public class StorageProxy implements StorageProxyMBean long startTime = nanoTime(); + ClusterMetadata metadata = ClusterMetadata.current(); try { @@ -1025,23 +1026,25 @@ public class StorageProxy implements StorageProxyMBean List wrappers = new ArrayList<>(mutations.size()); //non-local mutations rely on the base mutation commit-log entry for eventual consistency Set nonLocalMutations = new HashSet<>(mutations); - Token baseToken = StorageService.instance.getTokenMetadata().partitioner.getToken(dataKey); + Token baseToken = metadata.tokenMap.partitioner().getToken(dataKey); ConsistencyLevel consistencyLevel = ConsistencyLevel.ONE; //Since the base -> view replication is 1:1 we only need to store the BL locally - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forLocalBatchlogWrite(); + ReplicaPlan.ForWrite localReplicaPlan = ReplicaPlans.forLocalBatchlogWrite(); BatchlogCleanup cleanup = new BatchlogCleanup(mutations.size(), - () -> asyncRemoveFromBatchlog(replicaPlan, batchUUID)); + () -> asyncRemoveFromBatchlog(localReplicaPlan, batchUUID)); // add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet for (Mutation mutation : mutations) { String keyspaceName = mutation.getKeyspaceName(); Token tk = mutation.key().getToken(); - AbstractReplicationStrategy replicationStrategy = Keyspace.open(keyspaceName).getReplicationStrategy(); - Optional pairedEndpoint = ViewUtils.getViewNaturalEndpoint(replicationStrategy, baseToken, tk); - EndpointsForToken pendingReplicas = StorageService.instance.getTokenMetadata().pendingEndpointsForToken(tk, keyspaceName); + Function> pairedEndpointSupplier = (cm) -> ViewUtils.getViewNaturalEndpoint(cm, keyspaceName, baseToken, tk); + FunctionpendingReplicasSupplier = (cm) -> cm.pendingEndpointsFor(Keyspace.open(keyspaceName).getMetadata(), tk); + + Optional pairedEndpoint = pairedEndpointSupplier.apply(metadata); + VersionedEndpoints.ForToken pendingReplicas = pendingReplicasSupplier.apply(metadata); // if there are no paired endpoints there are probably range movements going on, so we write to the local batchlog to replay later if (!pairedEndpoint.isPresent()) @@ -1077,13 +1080,18 @@ public class StorageProxy implements StorageProxyMBean } else { - ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWrite(replicationStrategy, - EndpointsForToken.of(tk, pairedEndpoint.get()), - pendingReplicas); + Function computeReplicas = (cm) -> { + VersionedEndpoints.ForToken pending = pendingReplicasSupplier.apply(cm); + return ReplicaLayout.forTokenWrite(Keyspace.open(keyspaceName).getReplicationStrategy(), + EndpointsForToken.of(tk, pairedEndpointSupplier.apply(cm).get()), + pending.get()); + }; + + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(metadata, Keyspace.open(keyspaceName), consistencyLevel, computeReplicas, ReplicaPlans.writeAll); + wrappers.add(wrapViewBatchResponseHandler(mutation, consistencyLevel, - consistencyLevel, - liveAndDown, + replicaPlan, baseComplete, WriteType.BATCH, cleanup, @@ -1411,16 +1419,13 @@ public class StorageProxy implements StorageProxyMBean * Keeps track of ViewWriteMetrics */ private static WriteResponseHandlerWrapper wrapViewBatchResponseHandler(Mutation mutation, - ConsistencyLevel consistencyLevel, ConsistencyLevel batchConsistencyLevel, - ReplicaLayout.ForTokenWrite liveAndDown, + ReplicaPlan.ForWrite replicaPlan, AtomicLong baseComplete, WriteType writeType, BatchlogResponseHandler.BatchlogCleanup cleanup, long queryStartNanoTime) { - Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName()); - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, liveAndDown, ReplicaPlans.writeAll); AbstractReplicationStrategy replicationStrategy = replicaPlan.replicationStrategy(); AbstractWriteResponseHandler writeHandler = replicationStrategy.getWriteResponseHandler(replicaPlan, () -> { long delay = Math.max(0, currentTimeMillis() - baseComplete.get()); @@ -1711,7 +1716,8 @@ public class StorageProxy implements StorageProxyMBean */ public static AbstractWriteResponseHandler mutateCounter(CounterMutation cm, String localDataCenter, long queryStartNanoTime) throws UnavailableException, OverloadedException { - Replica replica = findSuitableReplica(cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency()); + ClusterMetadata metadata = ClusterMetadata.current(); + Replica replica = ReplicaPlans.findCounterLeaderReplica(metadata, cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency()); if (replica.isSelf()) { @@ -1725,15 +1731,17 @@ public class StorageProxy implements StorageProxyMBean Token tk = cm.key().getToken(); // we build this ONLY to perform the sufficiency check that happens on construction - ReplicaPlans.forWrite(keyspace, cm.consistency(), tk, ReplicaPlans.writeAll); + ReplicaPlans.forWrite(metadata, keyspace, cm.consistency(), tk, ReplicaPlans.writeAll); // This host isn't a replica, so mark the request as being remote. If this host is a // replica, applyCounterMutationOnCoordinator() in the branch above will call performWrite(), and // there we'll mark a local request against the metrics. writeMetrics.remoteRequests.mark(); + ReplicaPlan.ForWrite forWrite = ReplicaPlans.forForwardingCounterWrite(metadata, keyspace, tk, + clm -> ReplicaPlans.findCounterLeaderReplica(clm, cm.getKeyspaceName(), cm.key(), localDataCenter, cm.consistency())); // Forward the actual update to the chosen leader replica - AbstractWriteResponseHandler responseHandler = new WriteResponseHandler<>(ReplicaPlans.forForwardingCounterWrite(keyspace, tk, replica), + AbstractWriteResponseHandler responseHandler = new WriteResponseHandler<>(forWrite, WriteType.COUNTER, null, queryStartNanoTime); Tracing.trace("Enqueuing counter update to {}", replica); @@ -1743,53 +1751,6 @@ public class StorageProxy implements StorageProxyMBean } } - /** - * Find a suitable replica as leader for counter update. - * For now, we pick a random replica in the local DC (or ask the snitch if - * there is no replica alive in the local DC). - * TODO: if we track the latency of the counter writes (which makes sense - * contrarily to standard writes since there is a read involved), we could - * trust the dynamic snitch entirely, which may be a better solution. It - * is unclear we want to mix those latencies with read latencies, so this - * may be a bit involved. - */ - private static Replica findSuitableReplica(String keyspaceName, DecoratedKey key, String localDataCenter, ConsistencyLevel cl) throws UnavailableException - { - Keyspace keyspace = Keyspace.open(keyspaceName); - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - AbstractReplicationStrategy replicationStrategy = keyspace.getReplicationStrategy(); - EndpointsForToken replicas = replicationStrategy.getNaturalReplicasForToken(key); - - // CASSANDRA-13043: filter out those endpoints not accepting clients yet, maybe because still bootstrapping - replicas = replicas.filter(replica -> StorageService.instance.isRpcReady(replica.endpoint())); - - // CASSANDRA-17411: filter out endpoints that are not alive - replicas = replicas.filter(replica -> FailureDetector.instance.isAlive(replica.endpoint())); - - // TODO have a way to compute the consistency level - if (replicas.isEmpty()) - throw UnavailableException.create(cl, cl.blockFor(replicationStrategy), 0); - - List localReplicas = new ArrayList<>(replicas.size()); - - for (Replica replica : replicas) - if (snitch.getDatacenter(replica).equals(localDataCenter)) - localReplicas.add(replica); - - if (localReplicas.isEmpty()) - { - // If the consistency required is local then we should not involve other DCs - if (cl.isDatacenterLocal()) - throw UnavailableException.create(cl, cl.blockFor(replicationStrategy), 0); - - // No endpoint in local DC, pick the closest endpoint according to the snitch - replicas = snitch.sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), replicas); - return replicas.get(0); - } - - return localReplicas.get(ThreadLocalRandom.current().nextInt(localReplicas.size())); - } - // Must be called on a replica of the mutation. This replica becomes the // leader of this mutation. public static AbstractWriteResponseHandler applyCounterMutationOnLeader(CounterMutation cm, String localDataCenter, Runnable callback, long queryStartNanoTime) @@ -1846,15 +1807,6 @@ public class StorageProxy implements StorageProxyMBean public static PartitionIterator read(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, long queryStartNanoTime) throws UnavailableException, IsBootstrappingException, ReadFailureException, ReadTimeoutException, InvalidRequestException { - if (!isSafeToPerformRead(group.queries)) - { - readMetrics.unavailables.mark(); - readMetricsForLevel(consistencyLevel).unavailables.mark(); - IsBootstrappingException exception = new IsBootstrappingException(); - logRequestException(exception, group.queries); - throw exception; - } - if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistReadsEnabled()) { for (SinglePartitionReadCommand command : group.queries) @@ -1873,20 +1825,22 @@ public class StorageProxy implements StorageProxyMBean : readRegular(group, consistencyLevel, queryStartNanoTime); } - public static boolean isSafeToPerformRead(List queries) + public static boolean hasJoined() { - return isSafeToPerformRead() || systemKeyspaceQuery(queries); - } + ClusterMetadata metadata = ClusterMetadata.current(); + if (metadata == null) + return false; - public static boolean isSafeToPerformRead() - { - return !StorageService.instance.isBootstrapMode(); + if (metadata.myNodeId() == null) + return false; + + return metadata.myNodeState() == NodeState.JOINED; } private static PartitionIterator readWithPaxos(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, long queryStartNanoTime) throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException { - return Paxos.useV2() + return (Paxos.useV2() || group.metadata().keyspace.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) ? Paxos.read(group, consistencyLevel) : legacyReadWithPaxos(group, consistencyLevel, queryStartNanoTime); } @@ -2089,11 +2043,12 @@ public class StorageProxy implements StorageProxyMBean AbstractReadExecutor[] reads = new AbstractReadExecutor[cmdCount]; + ClusterMetadata metadata = ClusterMetadata.current(); // Get the replica locations, sorted by response time according to the snitch, and create a read executor // for type of speculation we'll use in this read for (int i=0; i ongoingBootstrap = new AtomicReference<>(); private static int getRingDelay() { @@ -306,24 +288,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } } - private static int getSchemaDelay() - { - String newdelay = BOOTSTRAP_SCHEMA_DELAY_MS.getString(); - if (newdelay != null) - { - logger.info("Overriding SCHEMA_DELAY_MILLIS to {}ms", newdelay); - return Integer.parseInt(newdelay); - } - else - { - return 30 * 1000; - } - } - - /* This abstraction maintains the token/endpoint metadata information */ - private TokenMetadata tokenMetadata = new TokenMetadata(); - - public volatile VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(tokenMetadata.partitioner); + public volatile VersionedValue.VersionedValueFactory valueFactory = + new VersionedValue.VersionedValueFactory(DatabaseDescriptor.getPartitioner()); private Thread drainOnShutdown = null; private volatile boolean isShutdown = false; @@ -339,6 +305,35 @@ public class StorageService extends NotificationBroadcasterSupport implements IE @VisibleForTesting // this is used for dtests only, see CASSANDRA-18152 public volatile boolean skipNotificationListeners = false; + private final java.util.function.Predicate anyOutOfRangeOpsRecorded + = keyspace -> keyspace.metric.outOfRangeTokenReads.getCount() > 0 + || keyspace.metric.outOfRangeTokenWrites.getCount() > 0 + || keyspace.metric.outOfRangeTokenPaxosRequests.getCount() > 0; + + private long[] getOutOfRangeOperationCounts(Keyspace keyspace) + { + return new long[] + { + keyspace.metric.outOfRangeTokenReads.getCount(), + keyspace.metric.outOfRangeTokenWrites.getCount(), + keyspace.metric.outOfRangeTokenPaxosRequests.getCount() + }; + } + + public Map getOutOfRangeOperationCounts() + { + return Schema.instance.getKeyspaces() + .stream() + .map(Keyspace::open) + .filter(anyOutOfRangeOpsRecorded) + .collect(Collectors.toMap(Keyspace::getName, this::getOutOfRangeOperationCounts)); + } + + public void incOutOfRangeOperationCount() + { + (isStarting() ? StorageMetrics.startupOpsForInvalidToken : StorageMetrics.totalOpsForInvalidToken).inc(); + } + /** @deprecated See CASSANDRA-12509 */ @Deprecated(since = "3.10") public boolean isInShutdownHook() @@ -367,58 +362,78 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public RangesAtEndpoint getReplicas(String keyspaceName, InetAddressAndPort endpoint) { - return Keyspace.open(keyspaceName).getReplicationStrategy().getAddressReplicas(endpoint); + return Keyspace.open(keyspaceName).getReplicationStrategy() + .getAddressReplicas(ClusterMetadata.current(), endpoint); } public List> getLocalRanges(String ks) { - InetAddressAndPort broadcastAddress = FBUtilities.getBroadcastAddressAndPort(); + InetAddressAndPort broadcastAddress = getBroadcastAddressAndPort(); Keyspace keyspace = Keyspace.open(ks); List> ranges = new ArrayList<>(); - for (Replica r : keyspace.getReplicationStrategy().getAddressReplicas(broadcastAddress)) + for (Replica r : keyspace.getReplicationStrategy().getAddressReplicas(ClusterMetadata.current(), + broadcastAddress)) ranges.add(r.range()); return ranges; } - public List> getLocalAndPendingRanges(String ks) + public Collection> getLocalAndPendingRanges(String ks) { - InetAddressAndPort broadcastAddress = FBUtilities.getBroadcastAddressAndPort(); - Keyspace keyspace = Keyspace.open(ks); - List> ranges = new ArrayList<>(); - for (Replica r : keyspace.getReplicationStrategy().getAddressReplicas(broadcastAddress)) - ranges.add(r.range()); - for (Replica r : getTokenMetadata().getPendingRanges(ks, broadcastAddress)) - ranges.add(r.range()); - return ranges; + return ClusterMetadata.current().localWriteRanges(Keyspace.open(ks).getMetadata()); } + public OwnedRanges getNormalizedLocalRanges(String keyspaceName, InetAddressAndPort broadcastAddress) + { + return new OwnedRanges(getReplicas(keyspaceName, broadcastAddress).ranges()); + } + + @Deprecated(since = "CEP-21") public Collection> getPrimaryRanges(String keyspace) { - return getPrimaryRangesForEndpoint(keyspace, FBUtilities.getBroadcastAddressAndPort()); + return getPrimaryRangesForEndpoint(keyspace, getBroadcastAddressAndPort()); } + @Deprecated(since = "CEP-21") + public Collection> getPrimaryRangesForEndpoint(String keyspace, InetAddressAndPort ep) + { + return TokenRingUtils.getPrimaryRangesForEndpoint(keyspace, ep); + } + + @Deprecated(since = "CEP-21") public Collection> getPrimaryRangesWithinDC(String keyspace) { - return getPrimaryRangeForEndpointWithinDC(keyspace, FBUtilities.getBroadcastAddressAndPort()); + return getPrimaryRangeForEndpointWithinDC(keyspace, getBroadcastAddressAndPort()); + } + + @Deprecated(since = "CEP-21") + public Collection> getLocalPrimaryRangeForEndpoint(InetAddressAndPort referenceEndpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId node = metadata.directory.peerId(referenceEndpoint); + if (node == null) + throw new IllegalArgumentException("Unknown endpoint " + referenceEndpoint); + return SizeEstimatesRecorder.getLocalPrimaryRange(metadata, node); + } + + @Deprecated(since = "CEP-21") + public Collection> getPrimaryRangeForEndpointWithinDC(String keyspace, InetAddressAndPort endpoint) + { + return TokenRingUtils.getPrimaryRangeForEndpointWithinDC(keyspace, endpoint); + } + + @Deprecated(since = "CEP-21") + public static List> getAllRanges(List sortedTokens) + { + return TokenRingUtils.getAllRanges(sortedTokens); } private final Set replicatingNodes = Sets.newConcurrentHashSet(); private CassandraDaemon daemon; - private InetAddressAndPort removingNode; - - /* Are we starting this node in bootstrap mode? */ - private volatile boolean isBootstrapMode; - /* we bootstrap but do NOT join the ring unless told to do so */ private boolean isSurveyMode = TEST_WRITE_SURVEY.getBoolean(false); /* true if node is rebuilding and receiving data */ - private final AtomicBoolean isRebuilding = new AtomicBoolean(); - private final AtomicBoolean isDecommissioning = new AtomicBoolean(); - private volatile boolean initialized = false; - private volatile boolean joined = false; - private volatile boolean gossipActive = false; private final AtomicBoolean authSetupCalled = new AtomicBoolean(false); private volatile boolean authSetupComplete = false; @@ -428,7 +443,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public enum Mode { STARTING, NORMAL, JOINING, JOINING_FAILED, LEAVING, DECOMMISSIONED, DECOMMISSION_FAILED, MOVING, DRAINING, DRAINED } private volatile Mode operationMode = Mode.STARTING; - /* Used for tracking drain progress */ + /* Can currently hold DECOMMISSIONED, DECOMMISSION_FAILED, DRAINING, DRAINED for legacy compatibility. */ + private volatile Optional transientMode = Optional.empty(); + private volatile int totalCFs, remainingCFs; private static final AtomicInteger nextRepairCommand = new AtomicInteger(); @@ -437,15 +454,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private final String jmxObjectName; - private Collection bootstrapTokens = null; - // true when keeping strict consistency while bootstrapping public static final boolean useStrictConsistency = CONSISTENT_RANGE_MOVEMENT.getBoolean(); - private static final boolean allowSimultaneousMoves = CONSISTENT_SIMULTANEOUS_MOVES_ALLOW.getBoolean(); - private static final boolean joinRing = JOIN_RING.getBoolean(); - private boolean replacing; + private boolean joinRing = JOIN_RING.getBoolean(); - private final StreamStateStore streamStateStore = new StreamStateStore(); + final StreamStateStore streamStateStore = new StreamStateStore(); public final SSTablesGlobalTracker sstablesTracker; @@ -454,36 +467,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return isSurveyMode; } - public boolean hasJoined() - { - return joined; - } - - /** - * This method updates the local token on disk - */ - public void setTokens(Collection tokens) - { - assert tokens != null && !tokens.isEmpty() : "Node needs at least one token."; - if (logger.isDebugEnabled()) - logger.debug("Setting tokens to {}", tokens); - SystemKeyspace.updateTokens(tokens); - Collection localTokens = getLocalTokens(); - setGossipTokens(localTokens); - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); - setMode(Mode.NORMAL, false); - invalidateLocalRanges(); - } - - public void setGossipTokens(Collection tokens) - { - List> states = new ArrayList>(); - states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(tokens))); - states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, valueFactory.normal(tokens))); - states.add(Pair.create(ApplicationState.STATUS, valueFactory.normal(tokens))); - Gossiper.instance.addLocalApplicationStates(states); - } - public StorageService() { // use dedicated executor for handling JMX notifications @@ -492,6 +475,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE jmxObjectName = "org.apache.cassandra.db:type=StorageService"; sstablesTracker = new SSTablesGlobalTracker(DatabaseDescriptor.getSelectedSSTableFormat()); + registerMBeans(); } private void registerMBeans() @@ -518,7 +502,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE // should only be called via JMX public void stopGossiping() { - if (gossipActive) + if (isGossipRunning()) { if (!isNormal() && joinRing) throw new IllegalStateException("Unable to stop gossip because the node is not in the normal state. Try to stop the node instead."); @@ -531,14 +515,13 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } Gossiper.instance.stop(); - gossipActive = false; } } // should only be called via JMX public synchronized void startGossiping() { - if (!gossipActive) + if (!isGossipRunning()) { checkServiceAllowedToStart("gossip"); @@ -548,15 +531,21 @@ public class StorageService extends NotificationBroadcasterSupport implements IE boolean validTokens = tokens != null && !tokens.isEmpty(); // shouldn't be called before these are set if we intend to join the ring/are in the process of doing so - if (joined || joinRing) + if (!isStarting() || joinRing) assert validTokens : "Cannot start gossiping for a node intended to join without valid tokens"; if (validTokens) - setGossipTokens(tokens); + { + List> states = new ArrayList<>(); + states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(tokens))); + states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, valueFactory.normal(tokens))); + states.add(Pair.create(ApplicationState.STATUS, valueFactory.normal(tokens))); + logger.info("Node {} jump to NORMAL", getBroadcastAddressAndPort()); + Gossiper.instance.addLocalApplicationStates(states); + } Gossiper.instance.forceNewerGeneration(); - Gossiper.instance.start((int) (currentTimeMillis() / 1000)); - gossipActive = true; + Gossiper.instance.start((int) (currentTimeMillis() / 1000), true); } } @@ -632,7 +621,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE /** * Set the Gossip flag RPC_READY to false and then * shutdown the client services (thrift and CQL). - * + *

    * Note that other nodes will do this for us when * they get the Gossip shutdown message, so even if * we don't get time to broadcast this, it is not a problem. @@ -655,6 +644,15 @@ public class StorageService extends NotificationBroadcasterSupport implements IE Stage.shutdownNow(); } + /** + * Only used in jvm dtest when not using GOSSIP. + * See org.apache.cassandra.distributed.impl.Instance#startup(org.apache.cassandra.distributed.api.ICluster) + */ + public void unsafeSetInitialized() + { + initialized = true; + } + public boolean isInitialized() { return initialized; @@ -662,7 +660,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public boolean isGossipActive() { - return gossipActive; + return isGossipRunning(); } public boolean isDaemonSetupCompleted() @@ -677,214 +675,10 @@ public class StorageService extends NotificationBroadcasterSupport implements IE daemon.deactivate(); } - private synchronized UUID prepareForReplacement() throws ConfigurationException - { - if (SystemKeyspace.bootstrapComplete()) - throw new RuntimeException("Cannot replace address with a node that is already bootstrapped"); - - if (!joinRing) - throw new ConfigurationException("Cannot set both join_ring=false and attempt to replace a node"); - - if (!shouldBootstrap() && !ALLOW_UNSAFE_REPLACE.getBoolean()) - throw new RuntimeException("Replacing a node without bootstrapping risks invalidating consistency " + - "guarantees as the expected data may not be present until repair is run. " + - "To perform this operation, please restart with " + - "-D" + ALLOW_UNSAFE_REPLACE.getKey() + "=true"); - - InetAddressAndPort replaceAddress = DatabaseDescriptor.getReplaceAddress(); - logger.info("Gathering node replacement information for {}", replaceAddress); - Map epStates = Gossiper.instance.doShadowRound(); - // as we've completed the shadow round of gossip, we should be able to find the node we're replacing - EndpointState state = epStates.get(replaceAddress); - if (state == null) - throw new RuntimeException(String.format("Cannot replace_address %s because it doesn't exist in gossip", replaceAddress)); - - validateEndpointSnitch(epStates.values().iterator()); - - try - { - VersionedValue tokensVersionedValue = state.getApplicationState(ApplicationState.TOKENS); - if (tokensVersionedValue == null) - throw new RuntimeException(String.format("Could not find tokens for %s to replace", replaceAddress)); - - Collection tokens = TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(tokensVersionedValue.toBytes()))); - bootstrapTokens = validateReplacementBootstrapTokens(tokenMetadata, replaceAddress, tokens); - - if (state.isEmptyWithoutStatus() && REPLACEMENT_ALLOW_EMPTY.getBoolean()) - { - logger.warn("Gossip state not present for replacing node {}. Adding temporary entry to continue.", replaceAddress); - - // When replacing a node, we take ownership of all its tokens. - // If that node is currently down and not present in the gossip info - // of any other live peers, then we will not be able to take ownership - // of its tokens during bootstrap as they have no way of being propagated - // to this node's TokenMetadata. TM is loaded at startup (in which case - // it will be/ empty for a new replacement node) and only updated with - // tokens for an endpoint during normal state propagation (which will not - // occur if no peers have gossip state for it). - // However, the presence of host id and tokens in the system tables implies - // that the node managed to complete bootstrap at some point in the past. - // Peers may include this information loaded directly from system tables - // in a GossipDigestAck *only if* the GossipDigestSyn was sent as part of a - // shadow round (otherwise, a GossipDigestAck contains only state about peers - // learned via gossip). - // It is safe to do this here as since we completed a shadow round we know - // that : - // * replaceAddress successfully bootstrapped at some point and owned these - // tokens - // * we know that no other node currently owns these tokens - // * we are going to completely take over replaceAddress's ownership of - // these tokens. - tokenMetadata.updateNormalTokens(bootstrapTokens, replaceAddress); - UUID hostId = Gossiper.instance.getHostId(replaceAddress, epStates); - if (hostId != null) - tokenMetadata.updateHostId(hostId, replaceAddress); - - // If we were only able to learn about the node being replaced through the - // shadow gossip round (i.e. there is no state in gossip across the cluster - // about it, perhaps because the entire cluster has been bounced since it went - // down), then we're safe to proceed with the replacement. In this case, there - // will be no local endpoint state as we discard the results of the shadow - // round after preparing replacement info. We inject a minimal EndpointState - // to keep FailureDetector::isAlive and Gossiper::compareEndpointStartup from - // failing later in the replacement, as they both expect the replaced node to - // be fully present in gossip. - // Otherwise, if the replaced node is present in gossip, we need check that - // it is not in fact live. - // We choose to not include the EndpointState provided during the shadow round - // as its possible to include more state than is desired, so by creating a - // new empty endpoint without that information we can control what is in our - // local gossip state - Gossiper.instance.initializeUnreachableNodeUnsafe(replaceAddress); - } - } - catch (IOException e) - { - throw new RuntimeException(e); - } - - UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId(); - - if (isReplacingSameAddress()) - { - localHostId = Gossiper.instance.getHostId(replaceAddress, epStates); - SystemKeyspace.setLocalHostId(localHostId); // use the replacee's host Id as our own so we receive hints, etc - } - - return localHostId; - } - - private static Collection validateReplacementBootstrapTokens(TokenMetadata tokenMetadata, - InetAddressAndPort replaceAddress, - Collection bootstrapTokens) - { - Map conflicts = new HashMap<>(); - for (Token token : bootstrapTokens) - { - InetAddressAndPort conflict = tokenMetadata.getEndpoint(token); - if (null != conflict && !conflict.equals(replaceAddress)) - conflicts.put(token, tokenMetadata.getEndpoint(token)); - } - - if (!conflicts.isEmpty()) - { - String error = String.format("Conflicting token ownership information detected between " + - "gossip and current ring view during proposed replacement " + - "of %s. Some tokens identified in gossip for the node being " + - "replaced are currently owned by other peers: %s", - replaceAddress, - conflicts.entrySet() - .stream() - .map(e -> e.getKey() + "(" + e.getValue() + ")" ) - .collect(Collectors.joining(","))); - throw new RuntimeException(error); - - } - return bootstrapTokens; - } - - public synchronized void checkForEndpointCollision(UUID localHostId, Set peers) throws ConfigurationException - { - if (ALLOW_UNSAFE_JOIN.getBoolean()) - { - logger.warn("Skipping endpoint collision check as " + ALLOW_UNSAFE_JOIN.getKey() + "=true"); - return; - } - - logger.debug("Starting shadow gossip round to check for endpoint collision"); - Map epStates = Gossiper.instance.doShadowRound(peers); - - if (epStates.isEmpty() && DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort())) - logger.info("Unable to gossip with any peers but continuing anyway since node is in its own seed list"); - - // If bootstrapping, check whether any previously known status for the endpoint makes it unsafe to do so. - // If not bootstrapping, compare the host id for this endpoint learned from gossip (if any) with the local - // one, which was either read from system.local or generated at startup. If a learned id is present & - // doesn't match the local, then the node needs replacing - if (!Gossiper.instance.isSafeForStartup(FBUtilities.getBroadcastAddressAndPort(), localHostId, shouldBootstrap(), epStates)) - { - throw new RuntimeException(String.format("A node with address %s already exists, cancelling join. " + - "Use %s if you want to replace this node.", - FBUtilities.getBroadcastAddressAndPort(), REPLACE_ADDRESS.getKey())); - } - - validateEndpointSnitch(epStates.values().iterator()); - - if (shouldBootstrap() && useStrictConsistency && !allowSimultaneousMoves()) - { - for (Map.Entry entry : epStates.entrySet()) - { - // ignore local node or empty status - if (entry.getKey().equals(FBUtilities.getBroadcastAddressAndPort()) || (entry.getValue().getApplicationState(ApplicationState.STATUS_WITH_PORT) == null & entry.getValue().getApplicationState(ApplicationState.STATUS) == null)) - continue; - - VersionedValue value = entry.getValue().getApplicationState(ApplicationState.STATUS_WITH_PORT); - if (value == null) - { - value = entry.getValue().getApplicationState(ApplicationState.STATUS); - } - - String[] pieces = splitValue(value); - assert (pieces.length > 0); - String state = pieces[0]; - if (state.equals(VersionedValue.STATUS_BOOTSTRAPPING) || state.equals(VersionedValue.STATUS_LEAVING) || state.equals(VersionedValue.STATUS_MOVING)) - throw new UnsupportedOperationException("Other bootstrapping/leaving/moving nodes detected, cannot bootstrap while " + CONSISTENT_RANGE_MOVEMENT.getKey() + " is true"); - } - } - } - - private static void validateEndpointSnitch(Iterator endpointStates) - { - Set datacenters = new HashSet<>(); - Set racks = new HashSet<>(); - while (endpointStates.hasNext()) - { - EndpointState state = endpointStates.next(); - VersionedValue val = state.getApplicationState(ApplicationState.DC); - if (val != null) - datacenters.add(val.value); - val = state.getApplicationState(ApplicationState.RACK); - if (val != null) - racks.add(val.value); - } - - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - if (!snitch.validate(datacenters, racks)) - { - throw new IllegalStateException(); - } - } - - private boolean allowSimultaneousMoves() - { - return allowSimultaneousMoves && DatabaseDescriptor.getNumTokens() == 1; - } - // for testing only public void unsafeInitialize() throws ConfigurationException { initialized = true; - gossipActive = true; Gossiper.instance.register(this); Gossiper.instance.start((int) (currentTimeMillis() / 1000)); // needed for node-ring gathering. Gossiper.instance.addLocalApplicationState(ApplicationState.NET_VERSION, valueFactory.networkVersion()); @@ -892,16 +686,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } public synchronized void initServer() throws ConfigurationException - { - initServer(SCHEMA_DELAY_MILLIS, RING_DELAY_MILLIS); - } - - public synchronized void initServer(int schemaAndRingDelayMillis) throws ConfigurationException - { - initServer(schemaAndRingDelayMillis, RING_DELAY_MILLIS); - } - - public synchronized void initServer(int schemaTimeoutMillis, int ringTimeoutMillis) throws ConfigurationException { logger.info("Cassandra version: {}", FBUtilities.getReleaseVersionString()); logger.info("Git SHA: {}", FBUtilities.getGitSHA()); @@ -921,14 +705,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE throw new AssertionError(e); } - if (LOAD_RING_STATE.getBoolean()) - { - logger.info("Loading persisted ring state"); - populatePeerTokenMetadata(); - for (InetAddressAndPort endpoint : tokenMetadata.getAllEndpoints()) - Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.addSavedEndpoint(endpoint)); - } - // daemon threads, like our executors', continue to run while shutdown hooks are invoked drainOnShutdown = NamedThreadFactory.createThread(new WrappedRunnable() { @@ -952,99 +728,142 @@ public class StorageService extends NotificationBroadcasterSupport implements IE }, "StorageServiceShutdownHook"); Runtime.getRuntime().addShutdownHook(drainOnShutdown); - replacing = isReplacing(); - - if (!START_GOSSIP.getBoolean()) - { - logger.info("Not starting gossip as requested."); - completeInitialization(); - return; - } - - prepareToJoin(); - - // Has to be called after the host id has potentially changed in prepareToJoin(). + Schema.instance.saveSystemKeyspace(); + DatabaseDescriptor.getInternodeAuthenticator().setupInternode(); try { - CacheService.instance.counterCache.loadSavedAsync().get(); + MessagingService.instance().waitUntilListening(); } - catch (Throwable t) + catch (InterruptedException e) { - JVMStabilityInspector.inspectThrowable(t); - logger.warn("Error loading counter cache", t); + throw new RuntimeException("Could not finish waiting until listening", e); } - if (joinRing) + if (ClusterMetadataService.state() == ClusterMetadataService.State.GOSSIP) { - joinTokenRing(schemaTimeoutMillis, ringTimeoutMillis); + // register listener before starting gossiper to avoid missing messages + Gossiper.instance.register(new GossipCMSListener()); + } + sstablesTracker.register((notification, o) -> { + if (!(notification instanceof SSTablesVersionsInUseChangeNotification)) + return; + + Set versions = ((SSTablesVersionsInUseChangeNotification) notification).versionsInUse; + logger.debug("Updating local sstables version in Gossip to {}", versions); + + Gossiper.instance.addLocalApplicationState(ApplicationState.SSTABLE_VERSIONS, + valueFactory.sstableVersions(versions)); + }); + + if (SystemKeyspace.wasDecommissioned()) + throw new ConfigurationException("This node was decommissioned and will not rejoin the ring unless cassandra.override_decommission=true has been set, or all existing data is removed and the node is bootstrapped again"); + + if (DatabaseDescriptor.getReplaceTokens().size() > 0 || DatabaseDescriptor.getReplaceNode() != null) + throw new RuntimeException("Replace method removed; use cassandra.replace_address instead"); + + if (isReplacing()) + { + if (SystemKeyspace.bootstrapComplete()) + throw new RuntimeException("Cannot replace with a node that is already bootstrapped"); + + InetAddressAndPort replaceAddress = DatabaseDescriptor.getReplaceAddress(); + Directory directory = ClusterMetadata.current().directory; + if (directory.peerId(replaceAddress) == null || directory.peerState(replaceAddress) != JOINED) + throw new RuntimeException(String.format("Cannot replace node %s which is not currently joined", replaceAddress)); + + BootstrapAndReplace.checkUnsafeReplace(shouldBootstrap()); + } + + if (isReplacingSameAddress()) + { + BootstrapAndReplace.gossipStateToHibernate(ClusterMetadata.current(), ClusterMetadata.currentNullable().myNodeId()); + Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), false); } else { - Collection tokens = SystemKeyspace.getSavedTokens(); - if (!tokens.isEmpty()) - { - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); - // order is important here, the gossiper can fire in between adding these two states. It's ok to send TOKENS without STATUS, but *not* vice versa. - List> states = new ArrayList>(); - states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(tokens))); - states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, valueFactory.hibernate(true))); - states.add(Pair.create(ApplicationState.STATUS, valueFactory.hibernate(true))); - Gossiper.instance.addLocalApplicationStates(states); - } - doAuthSetup(true); - logger.info("Not joining ring as requested. Use JMX (StorageService->joinRing()) to initiate ring joining"); + Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), + ClusterMetadataService.state() != ClusterMetadataService.State.GOSSIP); // only populate local state if not running in gossip mode } + Gossiper.instance.register(this); + Gossiper.instance.addLocalApplicationState(ApplicationState.NET_VERSION, valueFactory.networkVersion()); + Gossiper.instance.addLocalApplicationState(ApplicationState.SSTABLE_VERSIONS, + valueFactory.sstableVersions(sstablesTracker.versionsInUse())); + + if (ClusterMetadataService.state() == ClusterMetadataService.State.REMOTE) + Gossiper.instance.triggerRoundWithCMS(); + + Gossiper.waitToSettle(); + + NodeId self; + if (isReplacingSameAddress()) + { + self = ClusterMetadata.current().myNodeId(); + if (self == null) + throw new IllegalStateException("Tried to replace same address, but node does not seem to be registered"); + } + else + { + self = Register.maybeRegister(); + } + + Startup.maybeExecuteStartupTransformation(self); + + try + { + if (joinRing) + joinRing(); + else + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (metadata.myNodeState() == JOINED) + BootstrapAndReplace.gossipStateToHibernate(metadata, metadata.myNodeId()); + + logger.info("Not joining ring as requested. Use JMX (StorageService->joinRing()) to initiate ring joining"); + } + } + catch (IOException e) + { + throw new RuntimeException("Could not perform startup sequence and join cluster", e); + } + + maybeInitializeServices(); completeInitialization(); } - @VisibleForTesting - public void completeInitialization() + private void completeInitialization() { - if (!initialized) - registerMBeans(); initialized = true; } - public void populateTokenMetadata() + public static boolean cancelInProgressSequences(NodeId sequenceOwner) { - if (LOAD_RING_STATE.getBoolean()) - { - populatePeerTokenMetadata(); - // if we have not completed bootstrapping, we should not add ourselves as a normal token - if (!shouldBootstrap()) - tokenMetadata.updateNormalTokens(SystemKeyspace.getSavedTokens(), FBUtilities.getBroadcastAddressAndPort()); - - logger.info("Token metadata: {}", tokenMetadata); - } + return ClusterMetadataService.instance() + .commit(new CancelInProgressSequence(sequenceOwner), + metadata -> true, + (code, message) -> { + logger.warn(String.format("Could not cancel in-progress sequence: %s", message)); + return false; + }); } - private void populatePeerTokenMetadata() + private boolean servicesInitialized = false; + public void maybeInitializeServices() { - logger.info("Populating token metadata from system tables"); - Multimap loadedTokens = SystemKeyspace.loadTokens(); + if (servicesInitialized) + return; - // entry has been mistakenly added, delete it - if (loadedTokens.containsKey(FBUtilities.getBroadcastAddressAndPort())) - SystemKeyspace.removeEndpoint(FBUtilities.getBroadcastAddressAndPort()); - - Map loadedHostIds = SystemKeyspace.loadHostIds(); - Map hostIdToEndpointMap = new HashMap<>(); - for (InetAddressAndPort ep : loadedTokens.keySet()) - { - UUID hostId = loadedHostIds.get(ep); - if (hostId != null) - hostIdToEndpointMap.put(hostId, ep); - } - tokenMetadata.updateNormalTokens(loadedTokens); - tokenMetadata.updateHostIds(hostIdToEndpointMap); + StorageProxy.instance.initialLoadPartitionDenylist(); + LoadBroadcaster.instance.startBroadcasting(); + DiskUsageBroadcaster.instance.startBroadcasting(); + HintsService.instance.startDispatch(); + BatchlogManager.instance.start(); + startSnapshotManager(); + servicesInitialized = true; } public boolean isReplacing() { - if (replacing) - return true; - if (REPLACE_ADDRESS_FIRST_BOOT.getString() != null && SystemKeyspace.bootstrapComplete()) { logger.info("Replace address on the first boot requested; this node is already bootstrapped"); @@ -1065,6 +884,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE Runtime.getRuntime().removeShutdownHook(drainOnShutdown); } + public boolean shouldJoinRing() + { + return joinRing; + } + private boolean shouldBootstrap() { return DatabaseDescriptor.isAutoBootstrap() && !SystemKeyspace.bootstrapComplete() && !isSeed(); @@ -1072,110 +896,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public static boolean isSeed() { - return DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort()); - } - - private void prepareToJoin() throws ConfigurationException - { - if (!joined) - { - Map appStates = new EnumMap<>(ApplicationState.class); - - if (SystemKeyspace.wasDecommissioned()) - { - if (OVERRIDE_DECOMMISSION.getBoolean()) - { - logger.warn("This node was decommissioned, but overriding by operator request."); - SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); - } - else - { - throw new ConfigurationException("This node was decommissioned and will not rejoin the ring unless -D" + OVERRIDE_DECOMMISSION.getKey() + - "=true has been set, or all existing data is removed and the node is bootstrapped again"); - } - } - - if (DatabaseDescriptor.getReplaceTokens().size() > 0 || DatabaseDescriptor.getReplaceNode() != null) - throw new RuntimeException("Replace method removed; use " + REPLACE_ADDRESS.getKey() + " system property instead."); - - DatabaseDescriptor.getInternodeAuthenticator().setupInternode(); - MessagingService.instance().listen(); - - UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId(); - - if (replacing) - { - localHostId = prepareForReplacement(); - appStates.put(ApplicationState.TOKENS, valueFactory.tokens(bootstrapTokens)); - - if (!shouldBootstrap()) - { - // Will not do replace procedure, persist the tokens we're taking over locally - // so that they don't get clobbered with auto generated ones in joinTokenRing - SystemKeyspace.updateTokens(bootstrapTokens); - } - else if (isReplacingSameAddress()) - { - //only go into hibernate state if replacing the same address (CASSANDRA-8523) - logger.warn("Writes will not be forwarded to this node during replacement because it has the same address as " + - "the node to be replaced ({}). If the previous node has been down for longer than max_hint_window, " + - "repair must be run after the replacement process in order to make this node consistent.", - DatabaseDescriptor.getReplaceAddress()); - appStates.put(ApplicationState.STATUS_WITH_PORT, valueFactory.hibernate(true)); - appStates.put(ApplicationState.STATUS, valueFactory.hibernate(true)); - } - } - else - { - checkForEndpointCollision(localHostId, SystemKeyspace.loadHostIds().keySet()); - if (SystemKeyspace.bootstrapComplete()) - { - Preconditions.checkState(!Config.isClientMode()); - // tokens are only ever saved to system.local after bootstrap has completed and we're joining the ring, - // or when token update operations (move, decom) are completed - Collection savedTokens = SystemKeyspace.getSavedTokens(); - if (!savedTokens.isEmpty()) - appStates.put(ApplicationState.TOKENS, valueFactory.tokens(savedTokens)); - } - } - - // have to start the gossip service before we can see any info on other nodes. this is necessary - // for bootstrap to get the load info it needs. - // (we won't be part of the storage ring though until we add a counterId to our state, below.) - // Seed the host ID-to-endpoint map with our own ID. - getTokenMetadata().updateHostId(localHostId, FBUtilities.getBroadcastAddressAndPort()); - appStates.put(ApplicationState.NET_VERSION, valueFactory.networkVersion()); - appStates.put(ApplicationState.HOST_ID, valueFactory.hostId(localHostId)); - appStates.put(ApplicationState.NATIVE_ADDRESS_AND_PORT, valueFactory.nativeaddressAndPort(FBUtilities.getBroadcastNativeAddressAndPort())); - appStates.put(ApplicationState.RPC_ADDRESS, valueFactory.rpcaddress(FBUtilities.getJustBroadcastNativeAddress())); - appStates.put(ApplicationState.RELEASE_VERSION, valueFactory.releaseVersion()); - appStates.put(ApplicationState.SSTABLE_VERSIONS, valueFactory.sstableVersions(sstablesTracker.versionsInUse())); - - logger.info("Starting up server gossip"); - Gossiper.instance.register(this); - Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), appStates); // needed for node-ring gathering. - gossipActive = true; - - sstablesTracker.register((notification, o) -> { - if (!(notification instanceof SSTablesVersionsInUseChangeNotification)) - return; - - Set versions = ((SSTablesVersionsInUseChangeNotification)notification).versionsInUse; - logger.debug("Updating local sstables version in Gossip to {}", versions); - - Gossiper.instance.addLocalApplicationState(ApplicationState.SSTABLE_VERSIONS, - valueFactory.sstableVersions(versions)); - }); - - // gossip snitch infos (local DC and rack) - gossipSnitchInfo(); - Schema.instance.startSync(); - LoadBroadcaster.instance.startBroadcasting(); - DiskUsageBroadcaster.instance.startBroadcasting(); - HintsService.instance.startDispatch(); - BatchlogManager.instance.start(); - startSnapshotManager(); - } + return DatabaseDescriptor.getSeeds().contains(getBroadcastAddressAndPort()); } @VisibleForTesting @@ -1184,198 +905,186 @@ public class StorageService extends NotificationBroadcasterSupport implements IE snapshotManager.start(); } - public void waitForSchema(long schemaTimeoutMillis, long ringTimeoutMillis) - { - Instant deadline = FBUtilities.now().plus(java.time.Duration.ofMillis(ringTimeoutMillis)); - - while (Schema.instance.isEmpty() && FBUtilities.now().isBefore(deadline)) - Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); - - if (!Schema.instance.waitUntilReady(java.time.Duration.ofMillis(schemaTimeoutMillis))) - throw new IllegalStateException("Could not achieve schema readiness in " + java.time.Duration.ofMillis(schemaTimeoutMillis)); - } - - private void joinTokenRing(long schemaTimeoutMillis, long ringTimeoutMillis) throws ConfigurationException - { - joinTokenRing(!isSurveyMode, shouldBootstrap(), schemaTimeoutMillis, INDEFINITE, ringTimeoutMillis); - } - - @VisibleForTesting - public void joinTokenRing(boolean finishJoiningRing, - boolean shouldBootstrap, - long schemaTimeoutMillis, - long bootstrapTimeoutMillis, - long ringTimeoutMillis) throws ConfigurationException - { - joined = true; - - // We bootstrap if we haven't successfully bootstrapped before, as long as we are not a seed. - // If we are a seed, or if the user manually sets auto_bootstrap to false, - // we'll skip streaming data from other nodes and jump directly into the ring. - // - // The seed check allows us to skip the RING_DELAY sleep for the single-node cluster case, - // which is useful for both new users and testing. - // - // We attempted to replace this with a schema-presence check, but you need a meaningful sleep - // to get schema info from gossip which defeats the purpose. See CASSANDRA-4427 for the gory details. - Set current = new HashSet<>(); - if (logger.isDebugEnabled()) - { - logger.debug("Bootstrap variables: {} {} {} {}", - DatabaseDescriptor.isAutoBootstrap(), - SystemKeyspace.bootstrapInProgress(), - SystemKeyspace.bootstrapComplete(), - DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort())); - } - if (DatabaseDescriptor.isAutoBootstrap() && !SystemKeyspace.bootstrapComplete() && DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort())) - { - logger.info("This node will not auto bootstrap because it is configured to be a seed node."); - } - - boolean dataAvailable = true; // make this to false when bootstrap streaming failed - - if (shouldBootstrap) - { - current.addAll(prepareForBootstrap(schemaTimeoutMillis, ringTimeoutMillis)); - dataAvailable = bootstrap(bootstrapTokens, bootstrapTimeoutMillis); - } - else - { - bootstrapTokens = SystemKeyspace.getSavedTokens(); - if (bootstrapTokens.isEmpty()) - { - bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); - } - else - { - if (bootstrapTokens.size() != DatabaseDescriptor.getNumTokens()) - throw new ConfigurationException("Cannot change the number of tokens from " + bootstrapTokens.size() + " to " + DatabaseDescriptor.getNumTokens()); - else - logger.info("Using saved tokens {}", bootstrapTokens); - } - } - - setUpDistributedSystemKeyspaces(); - - if (finishJoiningRing) - { - if (dataAvailable) - { - finishJoiningRing(shouldBootstrap, bootstrapTokens); - // remove the existing info about the replaced node. - if (!current.isEmpty()) - { - Gossiper.runInGossipStageBlocking(() -> { - for (InetAddressAndPort existing : current) - Gossiper.instance.replacedEndpoint(existing); - }); - } - } - else - { - logger.warn("Some data streaming failed. Use nodetool to check bootstrap state and resume. For more, see `nodetool help bootstrap`. {}", SystemKeyspace.getBootstrapState()); - } - - StorageProxy.instance.initialLoadPartitionDenylist(); - } - else - { - if (dataAvailable) - logger.info("Startup complete, but write survey mode is active, not becoming an active ring member. Use JMX (StorageService->joinRing()) to finalize ring joining."); - else - logger.warn("Some data streaming failed. Use nodetool to check bootstrap state and resume. For more, see `nodetool help bootstrap`. {}", SystemKeyspace.getBootstrapState()); - } - } - public static boolean isReplacingSameAddress() { InetAddressAndPort replaceAddress = DatabaseDescriptor.getReplaceAddress(); - return replaceAddress != null && replaceAddress.equals(FBUtilities.getBroadcastAddressAndPort()); + return replaceAddress != null && replaceAddress.equals(getBroadcastAddressAndPort()); } - public void gossipSnitchInfo() + public synchronized void joinRing() throws IOException { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - String dc = snitch.getLocalDatacenter(); - String rack = snitch.getLocalRack(); - Gossiper.instance.addLocalApplicationState(ApplicationState.DC, StorageService.instance.valueFactory.datacenter(dc)); - Gossiper.instance.addLocalApplicationState(ApplicationState.RACK, StorageService.instance.valueFactory.rack(rack)); - } - - public void joinRing() throws IOException - { - SystemKeyspace.BootstrapState state = SystemKeyspace.getBootstrapState(); - joinRing(state.equals(SystemKeyspace.BootstrapState.IN_PROGRESS)); - } - - private synchronized void joinRing(boolean resumedBootstrap) throws IOException - { - if (!joined) + if (isStarting()) { - logger.info("Joining ring by operator request"); + // Node was started with -Dcassandra.join_ring=false before joining, so it has never + // begun the join process. + if (!joinRing) + { + logger.info("Joining ring by operator request"); + joinRing = true; + } try { - joinTokenRing(SCHEMA_DELAY_MILLIS, 0); - doAuthSetup(false); + org.apache.cassandra.tcm.Startup.startup(!isSurveyMode, shouldBootstrap(), isReplacing()); } catch (ConfigurationException e) { throw new IOException(e.getMessage()); } } + else if (!joinRing) + { + // Previously joined node was restarted with -Dcassandra.join_ring=false and so started + // with `hibernate` status. Bring it out of that state now, but don't do anything else + // as the join/replace process has already completed. + if (readyToFinishJoiningRing()) + { + logger.info("Joining ring by operator request"); + joinRing = true; + ClusterMetadata metadata = ClusterMetadata.current(); + Gossiper.instance.mergeNodeToGossip(metadata.myNodeId(), metadata); + } + } else if (isSurveyMode) { - // if isSurveyMode is on then verify isBootstrapMode - // node can join the ring even if isBootstrapMode is true which should not happen - if (!isBootstrapMode()) + // if isSurveyMode then verify the node is in the right state to join the ring + // or that it has already done so + if (ClusterMetadata.current().myNodeState() == JOINED) + { + // note: this has always been a no-op, starting a previously joined node in + // survey mode is meaningless as bootstrapping and joining the ring is already + // complete and a full joined node being restarted in survey mode does not prevent + // it participating in reads. This exists only for backwards compatibilty. + logger.info("Leaving write survey mode and joining ring at operator request"); + isSurveyMode = false; + } + else if (!SystemKeyspace.bootstrapComplete()) + { + logger.warn("Can't join the ring because in write_survey mode and bootstrap hasn't completed"); + throw new IllegalStateException("Cannot join the ring until bootstrap completes"); + } + else if (readyToFinishJoiningRing()) { logger.info("Leaving write survey mode and joining ring at operator request"); - finishJoiningRing(resumedBootstrap, SystemKeyspace.getSavedTokens()); - doAuthSetup(false); + exitWriteSurveyMode(); isSurveyMode = false; daemon.start(); } else { logger.warn("Can't join the ring because in write_survey mode and bootstrap hasn't completed"); + throw new IllegalStateException("Cannot join the ring until bootstrap completes"); } } else if (isBootstrapMode()) { // bootstrap is not complete hence node cannot join the ring logger.warn("Can't join the ring because bootstrap hasn't completed."); + throw new IllegalStateException("Cannot join the ring until bootstrap completes"); } } - private void executePreJoinTasks(boolean bootstrap) + public void resumeBootstrapSequence() { - StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false) - .filter(cfs -> Schema.instance.getUserKeyspaces().contains(cfs.getKeyspaceName())) - .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(bootstrap)); + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId id = metadata.myNodeId(); + MultiStepOperation sequence = metadata.inProgressSequences.get(id); + + if (!(sequence instanceof BootstrapAndJoin) && !(sequence instanceof BootstrapAndReplace)) + throw new IllegalStateException("Can not resume bootstrap as join sequence has not been started"); + clearOngoingBootstrap(); + InProgressSequences.finishInProgressSequences(id); + // only start transports and report that we're done if the resumed bootstrap completed successfully + // See CASSANDRA-16491 + if (ongoingBootstrap.get() == null) + { + if (!isNativeTransportRunning()) + daemon.initializeClientTransports(); + daemon.start(); + progressSupport.progress("bootstrap", new ProgressEvent(ProgressEventType.COMPLETE, 1, 1, "Resume bootstrap complete")); + logger.info("Resume complete"); + } + } + + public boolean readyToFinishJoiningRing() + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId id = metadata.myNodeId(); + MultiStepOperation sequence = metadata.inProgressSequences.get(id); + + if (sequence == null && metadata.directory.peerState(id) == JOINED) + return true; + + if ((sequence.kind() == MultiStepOperation.Kind.JOIN && sequence.nextStep() == Transformation.Kind.MID_JOIN) + || (sequence.kind() == MultiStepOperation.Kind.REPLACE && sequence.nextStep() == Transformation.Kind.MID_REPLACE)) + { + return true; + } + + return false; + } + + /** + * Called when a node has been started in {@code write survey mode} on its first boot. In this case, the regular + * startup sequence, either joining with a new set of tokens or replacing an existing node, will pause after + * bootstrap streaming but before committing the MID_JOIN or MID_REPLACE step. This leaves the joining node as a + * fully up to date (if streaming was successful) write replica for the ranges it is acquiring, but it does not + * make it active for reads. + * At the point when an operator decides to bring the node out of write survey mode, we need to execute the + * remaining steps of the join/replace sequence. The caveat is that although this execution will recommence at the + * point it left off (after the START_JOIN/START_REPLACE step), the {@code finishJoiningRing} flag which causes + * execution to pause for write survey mode must be overridden, so that we fully execute the MID step. We also want + * force the {@code streamData} flag to false, to prevent re-streaming the bootstrap data. To do this, we create a + * temporary copy of the {@link MultiStepOperation} and manually execute its next step after verifying expected + * invariants. This causes the MID step to fully execute, which then moves the sequence persisted in + * {@link ClusterMetadata}'s in-progress sequences onto the FINISH step, and we can complete the operation in the + * normal way with {@link InProgressSequences#finishInProgressSequences(MultiStepOperation.SequenceKey)} + * */ + private void exitWriteSurveyMode() + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId id = metadata.myNodeId(); + MultiStepOperation sequence = metadata.inProgressSequences.get(id); + + // Double check the conditions we verified in readyToFinishJoiningRing + if (sequence.kind() != MultiStepOperation.Kind.JOIN && sequence.kind() != MultiStepOperation.Kind.REPLACE) + throw new IllegalStateException("Can not finish joining ring as join sequence has not been started"); + + if ((sequence.kind() == MultiStepOperation.Kind.JOIN && sequence.nextStep() != Transformation.Kind.MID_JOIN) + || (sequence.kind() == MultiStepOperation.Kind.REPLACE && sequence.nextStep() != Transformation.Kind.MID_REPLACE)) + { + throw new IllegalStateException("Can not finish joining ring, sequence is in an incorrect state. " + + "If no progress is made, cancel the join process for this node and retry"); + } + + if (sequence.kind() == MultiStepOperation.Kind.REPLACE && sequence.nextStep() != Transformation.Kind.MID_REPLACE) + throw new IllegalStateException("Can not finish joining ring, sequence is in an incorrect state. " + + "If no progress is made, cancel the join process for this node and retry"); + + // Create a temporary new copy of the sequence with the finishJoining flag set to true and with streaming + // disabled, then execute its next step (the MID_*). We do this because effectively we want to jump over the + // MID_JOIN/MID_REPLACE of the "real" sequence. Note, this does not replace the existing sequence in + // ClusterMetadata with the temporary copy, but an effect of executing the MID step of the copy is that it will + // update the persisted state of the sequence leaving it with only the FINISH_* step to complete. + Transformation.Kind next = sequence.nextStep(); + boolean success = (sequence instanceof BootstrapAndJoin) + ? ((BootstrapAndJoin)sequence).finishJoiningRing().executeNext().isContinuable() + : ((BootstrapAndReplace)sequence).finishJoiningRing().executeNext().isContinuable(); + + if (!success) + throw new RuntimeException(String.format("Could not perform next step of joining the ring {}, " + + "restart this node and inflight operations will attempt to complete. " + + "If no progress is made, cancel the join process for this node and retry", + next)); + + // Now the MID step has completed and updated the sequence persisted in ClusterMetadata, finish it. + InProgressSequences.finishInProgressSequences(id); } @VisibleForTesting - public void finishJoiningRing(boolean didBootstrap, Collection tokens) - { - // start participating in the ring. - setMode(Mode.JOINING, "Finish joining ring", true); - SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); - executePreJoinTasks(didBootstrap); - setTokens(tokens); - - assert tokenMetadata.sortedTokens().size() > 0; - } - - @VisibleForTesting - public void doAuthSetup(boolean setUpSchema) + public void doAuthSetup() { if (!authSetupCalled.getAndSet(true)) { - if (setUpSchema) - { - Schema.instance.transform(SchemaTransformations.updateSystemKeyspace(AuthKeyspace.metadata(), AuthKeyspace.GENERATION)); - } - DatabaseDescriptor.getRoleManager().setup(); DatabaseDescriptor.getAuthenticator().setup(); DatabaseDescriptor.getAuthorizer().setup(); @@ -1398,18 +1107,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return authSetupCalled.get(); } - - @VisibleForTesting - public void setUpDistributedSystemKeyspaces() - { - Schema.instance.transform(SchemaTransformations.updateSystemKeyspace(TraceKeyspace.metadata(), TraceKeyspace.GENERATION)); - Schema.instance.transform(SchemaTransformations.updateSystemKeyspace(SystemDistributedKeyspace.metadata(), SystemDistributedKeyspace.GENERATION)); - Schema.instance.transform(SchemaTransformations.updateSystemKeyspace(AuthKeyspace.metadata(), AuthKeyspace.GENERATION)); - } - public boolean isJoined() { - return tokenMetadata.isMember(FBUtilities.getBroadcastAddressAndPort()) && !isSurveyMode; + return ClusterMetadata.current().myNodeState() == JOINED && !isSurveyMode; } public void rebuild(String sourceDc) @@ -1424,151 +1124,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources, boolean excludeLocalDatacenterNodes) { - // fail if source DC is local and --exclude-local-dc is set - if (sourceDc != null && sourceDc.equals(DatabaseDescriptor.getLocalDataCenter()) && excludeLocalDatacenterNodes) - { - throw new IllegalArgumentException("Cannot set source data center to be local data center, when excludeLocalDataCenter flag is set"); - } - - if (sourceDc != null) - { - TokenMetadata.Topology topology = getTokenMetadata().cloneOnlyTokenMap().getTopology(); - Set availableDCs = topology.getDatacenterEndpoints().keySet(); - if (!availableDCs.contains(sourceDc)) - { - throw new IllegalArgumentException(String.format("Provided datacenter '%s' is not a valid datacenter, available datacenters are: %s", - sourceDc, String.join(",", availableDCs))); - } - } - - if (keyspace == null && tokens != null) - { - throw new IllegalArgumentException("Cannot specify tokens without keyspace."); - } - - // check ongoing rebuild - if (!isRebuilding.compareAndSet(false, true)) - { - throw new IllegalStateException("Node is still rebuilding. Check nodetool netstats."); - } - - try - { - logger.info("rebuild from dc: {}, {}, {}", sourceDc == null ? "(any dc)" : sourceDc, - keyspace == null ? "(All keyspaces)" : keyspace, - tokens == null ? "(All tokens)" : tokens); - - repairPaxosForTopologyChange("rebuild"); - - RangeStreamer streamer = new RangeStreamer(tokenMetadata, - null, - FBUtilities.getBroadcastAddressAndPort(), - StreamOperation.REBUILD, - useStrictConsistency && !replacing, - DatabaseDescriptor.getEndpointSnitch(), - streamStateStore, - false, - DatabaseDescriptor.getStreamingConnectionsPerHost()); - if (sourceDc != null) - streamer.addSourceFilter(new RangeStreamer.SingleDatacenterFilter(DatabaseDescriptor.getEndpointSnitch(), sourceDc)); - - if (excludeLocalDatacenterNodes) - streamer.addSourceFilter(new RangeStreamer.ExcludeLocalDatacenterFilter(DatabaseDescriptor.getEndpointSnitch())); - - if (keyspace == null) - { - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - streamer.addRanges(keyspaceName, getLocalReplicas(keyspaceName)); - } - else if (tokens == null) - { - streamer.addRanges(keyspace, getLocalReplicas(keyspace)); - } - else - { - Token.TokenFactory factory = getTokenFactory(); - List> ranges = new ArrayList<>(); - Pattern rangePattern = Pattern.compile("\\(\\s*(-?\\w+)\\s*,\\s*(-?\\w+)\\s*\\]"); - try (Scanner tokenScanner = new Scanner(tokens)) - { - while (tokenScanner.findInLine(rangePattern) != null) - { - MatchResult range = tokenScanner.match(); - Token startToken = factory.fromString(range.group(1)); - Token endToken = factory.fromString(range.group(2)); - logger.info("adding range: ({},{}]", startToken, endToken); - ranges.add(new Range<>(startToken, endToken)); - } - if (tokenScanner.hasNext()) - throw new IllegalArgumentException("Unexpected string: " + tokenScanner.next()); - } - - // Ensure all specified ranges are actually ranges owned by this host - RangesAtEndpoint localReplicas = getLocalReplicas(keyspace); - RangesAtEndpoint.Builder streamRanges = new RangesAtEndpoint.Builder(FBUtilities.getBroadcastAddressAndPort(), ranges.size()); - for (Range specifiedRange : ranges) - { - boolean foundParentRange = false; - for (Replica localReplica : localReplicas) - { - if (localReplica.contains(specifiedRange)) - { - streamRanges.add(localReplica.decorateSubrange(specifiedRange)); - foundParentRange = true; - break; - } - } - if (!foundParentRange) - { - throw new IllegalArgumentException(String.format("The specified range %s is not a range that is owned by this node. Please ensure that all token ranges specified to be rebuilt belong to this node.", specifiedRange.toString())); - } - } - - if (specificSources != null) - { - String[] stringHosts = specificSources.split(","); - Set sources = new HashSet<>(stringHosts.length); - for (String stringHost : stringHosts) - { - try - { - InetAddressAndPort endpoint = InetAddressAndPort.getByName(stringHost); - if (FBUtilities.getBroadcastAddressAndPort().equals(endpoint)) - { - throw new IllegalArgumentException("This host was specified as a source for rebuilding. Sources for a rebuild can only be other nodes in the cluster."); - } - sources.add(endpoint); - } - catch (UnknownHostException ex) - { - throw new IllegalArgumentException("Unknown host specified " + stringHost, ex); - } - } - streamer.addSourceFilter(new RangeStreamer.AllowedSourcesFilter(sources)); - } - - streamer.addRanges(keyspace, streamRanges.build()); - } - - StreamResultFuture resultFuture = streamer.fetchAsync(); - // wait for result - resultFuture.get(); - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } - catch (ExecutionException e) - { - // This is used exclusively through JMX, so log the full trace but only throw a simple RTE - logger.error("Error while rebuilding node", e.getCause()); - throw new RuntimeException("Error while rebuilding node: " + e.getCause().getMessage()); - } - finally - { - // rebuild is done (successfully or not) - isRebuilding.set(false); - } + Rebuild.rebuild(sourceDc, keyspace, tokens, specificSources, excludeLocalDatacenterNodes); } public void setRpcTimeout(long value) @@ -1639,7 +1195,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public void setInternodeStreamingTcpUserTimeoutInMS(int value) { - Preconditions.checkArgument(value >= 0, "TCP user timeout cannot be negative for internode streaming connection. Got %s", value); + checkArgument(value >= 0, "TCP user timeout cannot be negative for internode streaming connection. Got %s", value); DatabaseDescriptor.setInternodeStreamingTcpUserTimeoutInMS(value); logger.info("set internode streaming tcp user timeout to {} ms", value); } @@ -1932,226 +1488,29 @@ public class StorageService extends NotificationBroadcasterSupport implements IE DatabaseDescriptor.setIncrementalBackupsEnabled(value); } - @VisibleForTesting // only used by test - public void setMovingModeUnsafe() + public Future startBootstrap(ClusterMetadata metadata, + InetAddressAndPort beingReplaced, + MovementMap movements, + MovementMap strictMovements) { - setMode(Mode.MOVING, true); - } - - /** - * Only used in jvm dtest when not using GOSSIP. - * See org.apache.cassandra.distributed.impl.Instance#startup(org.apache.cassandra.distributed.api.ICluster) - */ - @VisibleForTesting - public void setNormalModeUnsafe() - { - setMode(Mode.NORMAL, true); - } - - private void setMode(Mode m, boolean log) - { - setMode(m, null, log); - } - - private void setMode(Mode m, String msg, boolean log) - { - operationMode = m; - String logMsg = msg == null ? m.toString() : String.format("%s: %s", m, msg); - if (log) - logger.info(logMsg); - else - logger.debug(logMsg); - } - - @VisibleForTesting - public Collection prepareForBootstrap(long schemaTimeoutMillis, long ringTimeoutMillis) - { - Set collisions = new HashSet<>(); - if (SystemKeyspace.bootstrapInProgress()) - logger.warn("Detected previous bootstrap failure; retrying"); - else - SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.IN_PROGRESS); - setMode(Mode.JOINING, "waiting for ring information", true); - waitForSchema(schemaTimeoutMillis, ringTimeoutMillis); - setMode(Mode.JOINING, "schema complete, ready to bootstrap", true); - setMode(Mode.JOINING, "waiting for pending range calculation", true); - PendingRangeCalculatorService.instance.blockUntilFinished(); - setMode(Mode.JOINING, "calculation complete, ready to bootstrap", true); - - logger.debug("... got ring + schema info"); - - if (useStrictConsistency && !allowSimultaneousMoves() && - ( - tokenMetadata.getBootstrapTokens().valueSet().size() > 0 || - tokenMetadata.getSizeOfLeavingEndpoints() > 0 || - tokenMetadata.getSizeOfMovingEndpoints() > 0 - )) - { - String bootstrapTokens = StringUtils.join(tokenMetadata.getBootstrapTokens().valueSet(), ','); - String leavingTokens = StringUtils.join(tokenMetadata.getLeavingEndpoints(), ','); - String movingTokens = StringUtils.join(tokenMetadata.getMovingEndpoints().stream().map(e -> e.right).toArray(), ','); - throw new UnsupportedOperationException(String.format("Other bootstrapping/leaving/moving nodes detected, cannot bootstrap while %s is true. Nodes detected, bootstrapping: %s; leaving: %s; moving: %s;", - CONSISTENT_RANGE_MOVEMENT.getKey(), bootstrapTokens, leavingTokens, movingTokens)); - } - - // get bootstrap tokens - if (!replacing) - { - if (tokenMetadata.isMember(FBUtilities.getBroadcastAddressAndPort())) - { - String s = "This node is already a member of the token ring; bootstrap aborted. (If replacing a dead node, remove the old one from the ring first.)"; - throw new UnsupportedOperationException(s); - } - setMode(Mode.JOINING, "getting bootstrap token", true); - bootstrapTokens = BootStrapper.getBootstrapTokens(tokenMetadata, FBUtilities.getBroadcastAddressAndPort(), schemaTimeoutMillis, ringTimeoutMillis); - } - else - { - if (!isReplacingSameAddress()) - { - // Historically BROADCAST_INTERVAL was used, but this is unrelated to ring_delay, so using it to know - // how long to sleep only works with the default settings (ring_delay=30s, broadcast=60s). For users - // who are aware of this relationship, this coupling should not be broken, but for most users this - // relationship isn't known and instead we should rely on the ring_delay. - // See CASSANDRA-17776 - long sleepDelayMillis = Math.max(LoadBroadcaster.BROADCAST_INTERVAL, ringTimeoutMillis * 2); - try - { - // Sleep additionally to make sure that the server actually is not alive - // and giving it more time to gossip if alive. - logger.info("Sleeping for {}ms waiting to make sure no new gossip updates happen for {}", sleepDelayMillis, DatabaseDescriptor.getReplaceAddress()); - Thread.sleep(sleepDelayMillis); - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } - - // check for operator errors... - long nanoDelay = MILLISECONDS.toNanos(ringTimeoutMillis); - for (Token token : bootstrapTokens) - { - InetAddressAndPort existing = tokenMetadata.getEndpoint(token); - if (existing != null) - { - EndpointState endpointStateForExisting = Gossiper.instance.getEndpointStateForEndpoint(existing); - long updateTimestamp = endpointStateForExisting.getUpdateTimestamp(); - long allowedDelay = nanoTime() - nanoDelay; - - // if the node was updated within the ring delay or the node is alive, we should fail - if (updateTimestamp > allowedDelay || endpointStateForExisting.isAlive()) - { - logger.error("Unable to replace node for token={}. The node is reporting as {}alive with updateTimestamp={}, allowedDelay={}", - token, endpointStateForExisting.isAlive() ? "" : "not ", updateTimestamp, allowedDelay); - throw new UnsupportedOperationException("Cannot replace a live node... "); - } - collisions.add(existing); - } - else - { - throw new UnsupportedOperationException("Cannot replace token " + token + " which does not exist!"); - } - } - } - else - { - try - { - Thread.sleep(RING_DELAY_MILLIS); - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } - - } - setMode(Mode.JOINING, "Replacing a node with token(s): " + bootstrapTokens, true); - } - return collisions; - } - - /** - * Bootstrap node by fetching data from other nodes. - * If node is bootstrapping as a new node, then this also announces bootstrapping to the cluster. - * - * This blocks until streaming is done. - * - * @param tokens bootstrapping tokens - * @return true if bootstrap succeeds. - */ - @VisibleForTesting - public boolean bootstrap(final Collection tokens, long bootstrapTimeoutMillis) - { - isBootstrapMode = true; - SystemKeyspace.updateTokens(tokens); // DON'T use setToken, that makes us part of the ring locally which is incorrect until we are done bootstrapping - - if (!replacing || !isReplacingSameAddress()) - { - // if not an existing token then bootstrap - List> states = new ArrayList<>(); - states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(tokens))); - states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, replacing? - valueFactory.bootReplacingWithPort(DatabaseDescriptor.getReplaceAddress()) : - valueFactory.bootstrapping(tokens))); - states.add(Pair.create(ApplicationState.STATUS, replacing ? - valueFactory.bootReplacing(DatabaseDescriptor.getReplaceAddress().getAddress()) : - valueFactory.bootstrapping(tokens))); - Gossiper.instance.addLocalApplicationStates(states); - setMode(Mode.JOINING, "sleeping " + RING_DELAY_MILLIS + " ms for pending range setup", true); - Uninterruptibles.sleepUninterruptibly(RING_DELAY_MILLIS, MILLISECONDS); - } - else - { - // Dont set any state for the node which is bootstrapping the existing token... - tokenMetadata.updateNormalTokens(tokens, FBUtilities.getBroadcastAddressAndPort()); - SystemKeyspace.removeEndpoint(DatabaseDescriptor.getReplaceAddress()); - } - if (!Gossiper.instance.seenAnySeed()) - throw new IllegalStateException("Unable to contact any seeds: " + Gossiper.instance.getSeeds()); - - if (RESET_BOOTSTRAP_PROGRESS.getBoolean()) - { - logger.info("Resetting bootstrap progress to start fresh"); - SystemKeyspace.resetAvailableStreamedRanges(); - } - - // Force disk boundary invalidation now that local tokens are set - invalidateLocalRanges(); - repairPaxosForTopologyChange("bootstrap"); - - Future bootstrapStream = startBootstrap(tokens); - try - { - if (bootstrapTimeoutMillis > 0) - bootstrapStream.get(bootstrapTimeoutMillis, MILLISECONDS); - else - bootstrapStream.get(); - bootstrapFinished(); - logger.info("Bootstrap completed for tokens {}", tokens); - return true; - } - catch (Throwable e) - { - logger.error("Error while waiting on bootstrap to complete. Bootstrap will have to be restarted.", e); - setMode(JOINING_FAILED, true); - return false; - } - } - - public Future startBootstrap(Collection tokens) - { - return startBootstrap(tokens, replacing); - } - - public Future startBootstrap(Collection tokens, boolean replacing) - { - setMode(Mode.JOINING, "Starting to bootstrap...", true); - BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, tokenMetadata); + logger.info("Starting to bootstrap..."); + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.IN_PROGRESS); + BootStrapper bootstrapper = new BootStrapper(getBroadcastAddressAndPort(), metadata, movements, strictMovements); + boolean res = ongoingBootstrap.compareAndSet(null, bootstrapper); + if (!res) + throw new IllegalStateException("Bootstrap can be started exactly once, but seems to have already started: " + bootstrapper); bootstrapper.addProgressListener(progressSupport); - return bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update + return bootstrapper.bootstrap(streamStateStore, + useStrictConsistency && beingReplaced == null, + beingReplaced); // handles token update } - private void invalidateLocalRanges() + public void clearOngoingBootstrap() + { + ongoingBootstrap.set(null); + } + + public void invalidateLocalRanges() { for (Keyspace keyspace : Keyspace.all()) { @@ -2168,24 +1527,15 @@ public class StorageService extends NotificationBroadcasterSupport implements IE /** * All MVs have been created during bootstrap, so mark them as built */ - private void markViewsAsBuilt() + public void markViewsAsBuilt() { - for (String keyspace : Schema.instance.getUserKeyspaces()) + for (KeyspaceMetadata keyspace : Schema.instance.getUserKeyspaces()) { - for (ViewMetadata view: Schema.instance.getKeyspaceMetadata(keyspace).views) + for (ViewMetadata view: keyspace.views) SystemKeyspace.finishViewBuildStatus(view.keyspace(), view.name()); } } - /** - * Called when bootstrap did finish successfully - */ - private void bootstrapFinished() - { - markViewsAsBuilt(); - isBootstrapMode = false; - } - @Override public String getBootstrapState() { @@ -2194,65 +1544,10 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public boolean resumeBootstrap() { - if (isBootstrapMode && SystemKeyspace.bootstrapInProgress()) + if (isBootstrapMode() && SystemKeyspace.bootstrapInProgress()) { logger.info("Resuming bootstrap..."); - - // get bootstrap tokens saved in system keyspace - final Collection tokens = SystemKeyspace.getSavedTokens(); - // already bootstrapped ranges are filtered during bootstrap - BootStrapper bootstrapper = new BootStrapper(FBUtilities.getBroadcastAddressAndPort(), tokens, tokenMetadata); - bootstrapper.addProgressListener(progressSupport); - Future bootstrapStream = bootstrapper.bootstrap(streamStateStore, useStrictConsistency && !replacing); // handles token update - bootstrapStream.addCallback(new FutureCallback() - { - @Override - public void onSuccess(StreamState streamState) - { - try - { - bootstrapFinished(); - if (isSurveyMode) - { - logger.info("Startup complete, but write survey mode is active, not becoming an active ring member. Use JMX (StorageService->joinRing()) to finalize ring joining."); - } - else - { - isSurveyMode = false; - progressSupport.progress("bootstrap", ProgressEvent.createNotification("Joining ring...")); - finishJoiningRing(true, bootstrapTokens); - doAuthSetup(false); - } - progressSupport.progress("bootstrap", new ProgressEvent(ProgressEventType.COMPLETE, 1, 1, "Resume bootstrap complete")); - if (!isNativeTransportRunning()) - daemon.initializeClientTransports(); - daemon.start(); - logger.info("Resume complete"); - } - catch(Exception e) - { - onFailure(e); - throw e; - } - } - - @Override - public void onFailure(Throwable e) - { - String message = "Error during bootstrap: "; - if (e instanceof ExecutionException && e.getCause() != null) - { - message += e.getCause().getMessage(); - } - else - { - message += e.getMessage(); - } - logger.error(message, e); - progressSupport.progress("bootstrap", new ProgressEvent(ProgressEventType.ERROR, 1, 1, message)); - progressSupport.progress("bootstrap", new ProgressEvent(ProgressEventType.COMPLETE, 1, 1, "Resume bootstrap complete")); - } - }); + resumeBootstrapSequence(); return true; } else @@ -2262,6 +1557,39 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } } + public void abortBootstrap(String nodeStr, String endpointStr) + { + logger.debug("Aborting bootstrap for {}/{}", nodeStr, endpointStr); + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId; + if (!StringUtils.isEmpty(nodeStr)) + nodeId = NodeId.fromString(nodeStr); + else + nodeId = metadata.directory.peerId(InetAddressAndPort.getByNameUnchecked(endpointStr)); + + InetAddressAndPort endpoint = metadata.directory.endpoint(nodeId); + if (Gossiper.instance.isKnownEndpoint(endpoint) && FailureDetector.instance.isAlive(endpoint)) + throw new RuntimeException("Can't abort bootstrap for " + nodeId + " - it is alive"); + NodeState nodeState = metadata.directory.peerState(nodeId); + switch (nodeState) + { + case REGISTERED: + case BOOTSTRAPPING: + case BOOT_REPLACING: + if (metadata.inProgressSequences.contains(nodeId)) + { + MultiStepOperation seq = metadata.inProgressSequences.get(nodeId); + if (seq.kind() != MultiStepOperation.Kind.JOIN && seq.kind() != MultiStepOperation.Kind.REPLACE) + throw new RuntimeException("Can't abort bootstrap for " + nodeId + " since it is not bootstrapping"); + ClusterMetadataService.instance().commit(new CancelInProgressSequence(nodeId)); + } + ClusterMetadataService.instance().commit(new Unregister(nodeId)); + break; + default: + throw new RuntimeException("Can't abort bootstrap for node " + nodeId + " since the state is " + nodeState); + } + } + public Map> getConcurrency(List stageNames) { Stream stageStream = stageNames.isEmpty() ? stream(Stage.values()) : stageNames.stream().map(Stage::fromPoolName); @@ -2279,12 +1607,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public boolean isBootstrapMode() { - return isBootstrapMode; - } - - public TokenMetadata getTokenMetadata() - { - return tokenMetadata; + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + return metadata != null && (metadata.myNodeState() == BOOTSTRAPPING || metadata.myNodeState() == BOOT_REPLACING); } public Map, List> getRangeToEndpointMap(String keyspace) @@ -2320,45 +1644,17 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ public String getNativeaddress(InetAddressAndPort endpoint, boolean withPort) { - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + if (endpoint.equals(getBroadcastAddressAndPort())) return FBUtilities.getBroadcastNativeAddressAndPort().getHostAddress(withPort); - else if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT) != null) - { - try - { - InetAddressAndPort address = InetAddressAndPort.getByName(Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT).value); - return address.getHostAddress(withPort); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } - else - { - final String ipAddress; - // If RPC_ADDRESS present in gossip for this endpoint use it. This is expected for 3.x nodes. - if (Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS) != null) - { - ipAddress = Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.RPC_ADDRESS).value; - } - else - { - // otherwise just use the IP of the endpoint itself. - ipAddress = endpoint.getHostAddress(false); - } - // include the configured native_transport_port. - try - { - InetAddressAndPort address = InetAddressAndPort.getByNameOverrideDefaults(ipAddress, DatabaseDescriptor.getNativeTransportPort()); - return address.getHostAddress(withPort); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } + ClusterMetadata metadata = ClusterMetadata.current(); + Directory directory = metadata.directory; + NodeId id = directory.peerId(endpoint); + if (id == null) + throw new RuntimeException("Unknown endpoint " + endpoint); + + NodeAddresses addresses = directory.getNodeAddresses(id); + return addresses.nativeAddress.getHostAddress(withPort); } public Map, List> getRangeToRpcaddressMap(String keyspace) @@ -2410,16 +1706,15 @@ public class StorageService extends NotificationBroadcasterSupport implements IE keyspace = Schema.instance.distributedKeyspaces().iterator().next().name; Map, List> map = new HashMap<>(); - for (Map.Entry, EndpointsForRange> entry : tokenMetadata.getPendingRangesMM(keyspace).asMap().entrySet()) - { - map.put(entry.getKey().asList(), Replicas.stringify(entry.getValue(), withPort)); - } + for (Entry, VersionedEndpoints.ForRange> entry : ClusterMetadata.current().pendingRanges(Keyspace.open(keyspace).getMetadata()).entrySet()) + map.put(entry.getKey().asList(), Replicas.stringify(entry.getValue().get(), withPort)); + return map; } public EndpointsByRange getRangeToAddressMap(String keyspace) { - return getRangeToAddressMap(keyspace, tokenMetadata.sortedTokens()); + return getRangeToAddressMap(keyspace, ClusterMetadata.current().tokenMap.tokens()); } public EndpointsByRange getRangeToAddressMapInLocalDC(String keyspace) @@ -2440,20 +1735,24 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private List getTokensInLocalDC() { List filteredTokens = Lists.newArrayList(); - for (Token token : tokenMetadata.sortedTokens()) + ClusterMetadata metadata = ClusterMetadata.current(); + for (Token token : metadata.tokenMap.tokens()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(token); - if (isLocalDC(endpoint)) + if (isLocalDC(metadata.tokenMap.owner(token), metadata)) filteredTokens.add(token); } return filteredTokens; } + private boolean isLocalDC(NodeId nodeId, ClusterMetadata metadata) + { + return metadata.directory.location(metadata.myNodeId()).datacenter.equals(metadata.directory.location(nodeId).datacenter); + } + private boolean isLocalDC(InetAddressAndPort targetHost) { - String remoteDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(targetHost); - String localDC = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); - return remoteDC.equals(localDC); + ClusterMetadata metadata = ClusterMetadata.current(); + return isLocalDC(metadata.directory.peerId(targetHost), metadata); } private EndpointsByRange getRangeToAddressMap(String keyspace, List sortedTokens) @@ -2518,14 +1817,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return describeRing(keyspace, false, false); } - /** - * The same as {@code describeRing(String)} but considers only the part of the ring formed by nodes in the local DC. - */ - public List describeLocalRing(String keyspace) throws InvalidRequestException - { - return describeRing(keyspace, true, false); - } - private List describeRing(String keyspace, boolean includeOnlyLocalDC, boolean withPort) throws InvalidRequestException { if (!Schema.instance.getKeyspaces().contains(keyspace)) @@ -2560,35 +1851,43 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private Map getTokenToEndpointMap(boolean withPort) { - Map mapInetAddress = tokenMetadata.getNormalAndBootstrappingTokenToEndpointMap(); + ClusterMetadata metadata = ClusterMetadata.current(); + Map mapNodeId = new HashMap<>(metadata.tokenMap.asMap()); + // we don't yet have the joining nodes in tokenmap - but they are required + // for nodetool status - grab them from the in progress sequences for the joining nodes; + for (NodeId nodeId : metadata.directory.peerIds()) + { + if (NodeState.isPreJoin(metadata.directory.peerState(nodeId))) + { + MultiStepOperation seq = metadata.inProgressSequences.get(nodeId); + GossipHelper.getTokensFromOperation(seq).forEach(t -> mapNodeId.put(t, nodeId)); + } + } // in order to preserve tokens in ascending order, we use LinkedHashMap here - Map mapString = new LinkedHashMap<>(mapInetAddress.size()); - List tokens = new ArrayList<>(mapInetAddress.keySet()); + Map mapString = new LinkedHashMap<>(mapNodeId.size()); + List tokens = new ArrayList<>(mapNodeId.keySet()); Collections.sort(tokens); for (Token token : tokens) { - mapString.put(token.toString(), mapInetAddress.get(token).getHostAddress(withPort)); + mapString.put(token.toString(), metadata.directory.endpoint(mapNodeId.get(token)).getHostAddress(withPort)); } return mapString; } public String getLocalHostId() { - UUID id = getLocalHostUUID(); - return id != null ? id.toString() : null; + return getLocalHostUUID().toString(); } public UUID getLocalHostUUID() { - UUID id = getTokenMetadata().getHostId(FBUtilities.getBroadcastAddressAndPort()); - if (id != null) - return id; - // this condition is to prevent accessing the tables when the node is not started yet, and in particular, - // when it is not going to be started at all (e.g. when running some unit tests or client tools). - else if ((DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized()) && CommitLog.instance.isStarted()) - return SystemKeyspace.getLocalHostId(); - - return null; + // Metadata collector requires using local host id, and flush of IndexInfo may race with + // creation and initialization of cluster metadata service. Metadata collector does accept + // null localhost ID values, it's just that TokenMetadata was created earlier. + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.peerId(getBroadcastAddressAndPort()) == null) + return null; + return metadata.directory.peerId(getBroadcastAddressAndPort()).toUUID(); } public Map getHostIdMap() @@ -2610,8 +1909,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private Map getEndpointToHostId(boolean withPort) { Map mapOut = new HashMap<>(); - for (Map.Entry entry : getTokenMetadata().getEndpointToHostIdMapForReading().entrySet()) - mapOut.put(entry.getKey().getHostAddress(withPort), entry.getValue().toString()); + for (Map.Entry entry : ClusterMetadata.current().directory.addresses.entrySet()) + mapOut.put(entry.getValue().broadcastAddress.getHostAddress(withPort), entry.getKey().toUUID().toString()); return mapOut; } @@ -2628,8 +1927,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private Map getHostIdToEndpoint(boolean withPort) { Map mapOut = new HashMap<>(); - for (Map.Entry entry : getTokenMetadata().getEndpointToHostIdMapForReading().entrySet()) - mapOut.put(entry.getValue().toString(), entry.getKey().getHostAddress(withPort)); + for (Map.Entry entry : ClusterMetadata.current().directory.addresses.entrySet()) + mapOut.put(entry.getKey().toUUID().toString(), entry.getValue().broadcastAddress.getHostAddress(withPort)); return mapOut; } @@ -2641,11 +1940,19 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ private EndpointsByRange constructRangeToEndpointMap(String keyspace, List> ranges) { - AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspace); + TokenMap tokenMap = metadata.tokenMap; + Map, EndpointsForRange> rangeToEndpointMap = new HashMap<>(ranges.size()); for (Range range : ranges) - rangeToEndpointMap.put(range, strategy.getNaturalReplicas(range.right)); + { + Token token = tokenMap.nextToken(tokenMap.tokens(), range.right.getToken()); + rangeToEndpointMap.put(range, metadata.placements.get(keyspaceMetadata.params.replication).reads.forRange(token).get()); + } + return new EndpointsByRange(rangeToEndpointMap); + } public void beforeChange(InetAddressAndPort endpoint, EndpointState currentState, ApplicationState newStateKey, VersionedValue newValue) @@ -2687,112 +1994,89 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ public void onChange(InetAddressAndPort endpoint, ApplicationState state, VersionedValue value) { - if (state == ApplicationState.STATUS || state == ApplicationState.STATUS_WITH_PORT) + EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); + + // Special case to handle moving to hibernate state, which we now need to trigger + // marking the node dead. Previously a node coming up in hibernate state would never + // be marked alive as it doesn't gossip a normal STATUS after a generation bump. Post + // CEP-21 however, peers already know about this hibernating node and as it gets + // marked up by virtue of actually being alive and responsive despite not gossiping a + // normal STATUS. + if (state == ApplicationState.STATUS_WITH_PORT) { String[] pieces = splitValue(value); - assert (pieces.length > 0); - - String moveName = pieces[0]; - - switch (moveName) + if (pieces[0].equals(VersionedValue.HIBERNATE)) { - case VersionedValue.STATUS_BOOTSTRAPPING_REPLACE: - handleStateBootreplacing(endpoint, pieces); + logger.info("Node {} state jump to hibernate", endpoint); + Gossiper.runInGossipStageBlocking(() -> { + Gossiper.instance.markDead(endpoint, epState); + }); + } + } + + if (epState == null || Gossiper.instance.isDeadState(epState)) + { + logger.debug("Ignoring state change for dead or unknown endpoint: {}", endpoint); + return; + } + + if (state == ApplicationState.INDEX_STATUS) + { + updateIndexStatus(endpoint, value); + return; + } + + if (ClusterMetadata.current().directory.allJoinedEndpoints().contains(endpoint)) + { + switch (state) + { + case RELEASE_VERSION: + SystemKeyspace.updatePeerInfo(endpoint, "release_version", value.value); break; - case VersionedValue.STATUS_BOOTSTRAPPING: - handleStateBootstrap(endpoint); + case RPC_ADDRESS: + try + { + SystemKeyspace.updatePeerInfo(endpoint, "rpc_address", InetAddress.getByName(value.value)); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } break; - case VersionedValue.STATUS_NORMAL: - handleStateNormal(endpoint, VersionedValue.STATUS_NORMAL); + case NATIVE_ADDRESS_AND_PORT: + try + { + InetAddressAndPort address = InetAddressAndPort.getByName(value.value); + SystemKeyspace.updatePeerNativeAddress(endpoint, address); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } break; - case VersionedValue.SHUTDOWN: - handleStateNormal(endpoint, VersionedValue.SHUTDOWN); + case RPC_READY: + notifyRpcChange(endpoint, epState.isRpcReady()); break; - case VersionedValue.REMOVING_TOKEN: - case VersionedValue.REMOVED_TOKEN: - handleStateRemoving(endpoint, pieces); + case NET_VERSION: + updateNetVersion(endpoint, value); break; - case VersionedValue.STATUS_LEAVING: - handleStateLeaving(endpoint); + case STATUS_WITH_PORT: + String[] pieces = splitValue(value); + String moveName = pieces[0]; + if (moveName.equals(VersionedValue.SHUTDOWN)) + logger.info("Node {} state jump to shutdown", endpoint); + else if (moveName.equals(VersionedValue.STATUS_NORMAL)) + logger.info("Node {} state jump to NORMAL", endpoint); break; - case VersionedValue.STATUS_LEFT: - handleStateLeft(endpoint, pieces); - break; - case VersionedValue.STATUS_MOVING: - handleStateMoving(endpoint, pieces); + case SCHEMA: + SystemKeyspace.updatePeerInfo(endpoint, "schema_version", UUID.fromString(value.value)); break; } } else { - if (state == ApplicationState.INDEX_STATUS) - { - updateIndexStatus(endpoint, value); - return; - } - - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (epState == null || Gossiper.instance.isDeadState(epState)) - { - logger.debug("Ignoring state change for dead or unknown endpoint: {}", endpoint); - return; - } - - if (getTokenMetadata().isMember(endpoint)) - { - switch (state) - { - case RELEASE_VERSION: - SystemKeyspace.updatePeerInfo(endpoint, "release_version", value.value); - break; - case DC: - updateTopology(endpoint); - SystemKeyspace.updatePeerInfo(endpoint, "data_center", value.value); - break; - case RACK: - updateTopology(endpoint); - SystemKeyspace.updatePeerInfo(endpoint, "rack", value.value); - break; - case RPC_ADDRESS: - try - { - SystemKeyspace.updatePeerInfo(endpoint, "rpc_address", InetAddress.getByName(value.value)); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case NATIVE_ADDRESS_AND_PORT: - try - { - InetAddressAndPort address = InetAddressAndPort.getByName(value.value); - SystemKeyspace.updatePeerNativeAddress(endpoint, address); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case SCHEMA: - SystemKeyspace.updatePeerInfo(endpoint, "schema_version", UUID.fromString(value.value)); - break; - case HOST_ID: - SystemKeyspace.updatePeerInfo(endpoint, "host_id", UUID.fromString(value.value)); - break; - case RPC_READY: - notifyRpcChange(endpoint, epState.isRpcReady()); - break; - case NET_VERSION: - updateNetVersion(endpoint, value); - break; - } - } - else - { - logger.debug("Ignoring application state {} from {} because it is not a member in token metadata", - state, endpoint); - } + logger.debug("Ignoring application state {} from {} because it is not a member in token metadata", + state, endpoint); } } @@ -2801,7 +2085,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return value.value.split(VersionedValue.DELIMITER_STR, -1); } - private void updateIndexStatus(InetAddressAndPort endpoint, VersionedValue versionedValue) + public static void updateIndexStatus(InetAddressAndPort endpoint, VersionedValue versionedValue) { IndexStatusManager.instance.receivePeerIndexStatus(endpoint, versionedValue); } @@ -2818,84 +2102,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } } - public void updateTopology(InetAddressAndPort endpoint) - { - if (getTokenMetadata().isMember(endpoint)) - { - getTokenMetadata().updateTopology(endpoint); - } - } - - public void updateTopology() - { - getTokenMetadata().updateTopology(); - } - - private void updatePeerInfo(InetAddressAndPort endpoint) - { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - InetAddress native_address = null; - int native_port = DatabaseDescriptor.getNativeTransportPort(); - - for (Map.Entry entry : epState.states()) - { - switch (entry.getKey()) - { - case RELEASE_VERSION: - SystemKeyspace.updatePeerInfo(endpoint, "release_version", entry.getValue().value); - break; - case DC: - SystemKeyspace.updatePeerInfo(endpoint, "data_center", entry.getValue().value); - break; - case RACK: - SystemKeyspace.updatePeerInfo(endpoint, "rack", entry.getValue().value); - break; - case RPC_ADDRESS: - try - { - native_address = InetAddress.getByName(entry.getValue().value); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case NATIVE_ADDRESS_AND_PORT: - try - { - InetAddressAndPort address = InetAddressAndPort.getByName(entry.getValue().value); - native_address = address.getAddress(); - native_port = address.getPort(); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - break; - case SCHEMA: - SystemKeyspace.updatePeerInfo(endpoint, "schema_version", UUID.fromString(entry.getValue().value)); - break; - case HOST_ID: - SystemKeyspace.updatePeerInfo(endpoint, "host_id", UUID.fromString(entry.getValue().value)); - break; - case INDEX_STATUS: - // Need to set the peer index status in SIM here - // to ensure the status is correct before the node - // fully joins the ring - updateIndexStatus(endpoint, entry.getValue()); - break; - } - } - - //Some tests won't set all the states - if (native_address != null) - { - SystemKeyspace.updatePeerNativeAddress(endpoint, - InetAddressAndPort.getByAddressOverrideDefaults(native_address, - native_port)); - } - } - private void notifyRpcChange(InetAddressAndPort endpoint, boolean ready) { if (ready) @@ -2919,33 +2125,24 @@ public class StorageService extends NotificationBroadcasterSupport implements IE subscriber.onDown(endpoint); } - private void notifyJoined(InetAddressAndPort endpoint) + public void notifyJoined(InetAddressAndPort endpoint) { - if (!isStatus(endpoint, VersionedValue.STATUS_NORMAL)) - return; - for (IEndpointLifecycleSubscriber subscriber : lifecycleSubscribers) subscriber.onJoinCluster(endpoint); } - private void notifyMoved(InetAddressAndPort endpoint) + public void notifyMoved(InetAddressAndPort endpoint) { for (IEndpointLifecycleSubscriber subscriber : lifecycleSubscribers) subscriber.onMove(endpoint); } - private void notifyLeft(InetAddressAndPort endpoint) + public void notifyLeft(InetAddressAndPort endpoint) { for (IEndpointLifecycleSubscriber subscriber : lifecycleSubscribers) subscriber.onLeaveCluster(endpoint); } - private boolean isStatus(InetAddressAndPort endpoint, String status) - { - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - return state != null && state.getStatus().equals(status); - } - public boolean isRpcReady(InetAddressAndPort endpoint) { EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); @@ -2958,767 +2155,18 @@ public class StorageService extends NotificationBroadcasterSupport implements IE * and there is no local endpoint state. In this case it's OK to just do nothing. Therefore, * we assert that the local endpoint state is not null only when value is true. * - * @param value - true indicates that RPC is ready, false indicates the opposite. + * @param isRpcReady - true indicates that RPC is ready, false indicates the opposite. */ - public void setRpcReady(boolean value) + public void setRpcReady(boolean isRpcReady) { - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(FBUtilities.getBroadcastAddressAndPort()); + EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(getBroadcastAddressAndPort()); // if value is false we're OK with a null state, if it is true we are not. - assert !value || state != null; + assert !isRpcReady || state != null; if (state != null) - Gossiper.instance.addLocalApplicationState(ApplicationState.RPC_READY, valueFactory.rpcReady(value)); + Gossiper.instance.addLocalApplicationState(ApplicationState.RPC_READY, valueFactory.rpcReady(isRpcReady)); } - private Collection getTokensFor(InetAddressAndPort endpoint) - { - try - { - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - if (state == null) - return Collections.emptyList(); - - VersionedValue versionedValue = state.getApplicationState(ApplicationState.TOKENS); - if (versionedValue == null) - return Collections.emptyList(); - - return TokenSerializer.deserialize(tokenMetadata.partitioner, new DataInputStream(new ByteArrayInputStream(versionedValue.toBytes()))); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - /** - * Handle node bootstrap - * - * @param endpoint bootstrapping node - */ - private void handleStateBootstrap(InetAddressAndPort endpoint) - { - Collection tokens; - // explicitly check for TOKENS, because a bootstrapping node might be bootstrapping in legacy mode; that is, not using vnodes and no token specified - tokens = getTokensFor(endpoint); - - if (logger.isDebugEnabled()) - logger.debug("Node {} state bootstrapping, token {}", endpoint, tokens); - - // if this node is present in token metadata, either we have missed intermediate states - // or the node had crashed. Print warning if needed, clear obsolete stuff and - // continue. - if (tokenMetadata.isMember(endpoint)) - { - // If isLeaving is false, we have missed both LEAVING and LEFT. However, if - // isLeaving is true, we have only missed LEFT. Waiting time between completing - // leave operation and rebootstrapping is relatively short, so the latter is quite - // common (not enough time for gossip to spread). Therefore we report only the - // former in the log. - if (!tokenMetadata.isLeaving(endpoint)) - logger.info("Node {} state jump to bootstrap", endpoint); - tokenMetadata.removeEndpoint(endpoint); - } - - tokenMetadata.addBootstrapTokens(tokens, endpoint); - PendingRangeCalculatorService.instance.update(); - - tokenMetadata.updateHostId(Gossiper.instance.getHostId(endpoint), endpoint); - } - - private void handleStateBootreplacing(InetAddressAndPort newNode, String[] pieces) - { - InetAddressAndPort oldNode; - try - { - oldNode = InetAddressAndPort.getByName(pieces[1]); - } - catch (Exception e) - { - logger.error("Node {} tried to replace malformed endpoint {}.", newNode, pieces[1], e); - return; - } - - if (FailureDetector.instance.isAlive(oldNode)) - { - throw new RuntimeException(String.format("Node %s is trying to replace alive node %s.", newNode, oldNode)); - } - - Optional replacingNode = tokenMetadata.getReplacingNode(newNode); - if (replacingNode.isPresent() && !replacingNode.get().equals(oldNode)) - { - throw new RuntimeException(String.format("Node %s is already replacing %s but is trying to replace %s.", - newNode, replacingNode.get(), oldNode)); - } - - Collection tokens = getTokensFor(newNode); - - if (logger.isDebugEnabled()) - logger.debug("Node {} is replacing {}, tokens {}", newNode, oldNode, tokens); - - tokenMetadata.addReplaceTokens(tokens, newNode, oldNode); - PendingRangeCalculatorService.instance.update(); - - tokenMetadata.updateHostId(Gossiper.instance.getHostId(newNode), newNode); - } - - private void ensureUpToDateTokenMetadata(String status, InetAddressAndPort endpoint) - { - Set tokens = new TreeSet<>(getTokensFor(endpoint)); - - if (logger.isDebugEnabled()) - logger.debug("Node {} state {}, tokens {}", endpoint, status, tokens); - - // If the node is previously unknown or tokens do not match, update tokenmetadata to - // have this node as 'normal' (it must have been using this token before the - // leave). This way we'll get pending ranges right. - if (!tokenMetadata.isMember(endpoint)) - { - logger.info("Node {} state jump to {}", endpoint, status); - updateTokenMetadata(endpoint, tokens); - } - else if (!tokens.equals(new TreeSet<>(tokenMetadata.getTokens(endpoint)))) - { - logger.warn("Node {} '{}' token mismatch. Long network partition?", endpoint, status); - updateTokenMetadata(endpoint, tokens); - } - } - - private void updateTokenMetadata(InetAddressAndPort endpoint, Iterable tokens) - { - updateTokenMetadata(endpoint, tokens, new HashSet<>()); - } - - private void updateTokenMetadata(InetAddressAndPort endpoint, Iterable tokens, Set endpointsToRemove) - { - Set tokensToUpdateInMetadata = new HashSet<>(); - Set tokensToUpdateInSystemKeyspace = new HashSet<>(); - - for (final Token token : tokens) - { - // we don't want to update if this node is responsible for the token and it has a later startup time than endpoint. - InetAddressAndPort currentOwner = tokenMetadata.getEndpoint(token); - if (currentOwner == null) - { - logger.debug("New node {} at token {}", endpoint, token); - tokensToUpdateInMetadata.add(token); - tokensToUpdateInSystemKeyspace.add(token); - } - else if (endpoint.equals(currentOwner)) - { - // set state back to normal, since the node may have tried to leave, but failed and is now back up - tokensToUpdateInMetadata.add(token); - tokensToUpdateInSystemKeyspace.add(token); - } - // Note: in test scenarios, there may not be any delta between the heartbeat generations of the old - // and new nodes, so we first check whether the new endpoint is marked as a replacement for the old. - else if (endpoint.equals(tokenMetadata.getReplacementNode(currentOwner).orElse(null)) || Gossiper.instance.compareEndpointStartup(endpoint, currentOwner) > 0) - { - tokensToUpdateInMetadata.add(token); - tokensToUpdateInSystemKeyspace.add(token); - - // currentOwner is no longer current, endpoint is. Keep track of these moves, because when - // a host no longer has any tokens, we'll want to remove it. - Multimap epToTokenCopy = getTokenMetadata().getEndpointToTokenMapForReading(); - epToTokenCopy.get(currentOwner).remove(token); - if (epToTokenCopy.get(currentOwner).isEmpty()) - endpointsToRemove.add(currentOwner); - - logger.info("Nodes {} and {} have the same token {}. {} is the new owner", endpoint, currentOwner, token, endpoint); - } - else - { - logger.info("Nodes {} and {} have the same token {}. Ignoring {}", endpoint, currentOwner, token, endpoint); - } - } - - tokenMetadata.updateNormalTokens(tokensToUpdateInMetadata, endpoint); - for (InetAddressAndPort ep : endpointsToRemove) - { - removeEndpoint(ep); - if (replacing && ep.equals(DatabaseDescriptor.getReplaceAddress())) - Gossiper.instance.replacementQuarantine(ep); // quarantine locally longer than normally; see CASSANDRA-8260 - } - if (!tokensToUpdateInSystemKeyspace.isEmpty()) - SystemKeyspace.updateTokens(endpoint, tokensToUpdateInSystemKeyspace); - - // Tokens changed, the local range ownership probably changed too. - invalidateLocalRanges(); - } - - @VisibleForTesting - public boolean isReplacingSameHostAddressAndHostId(UUID hostId) - { - try - { - return isReplacingSameAddress() && - Gossiper.instance.getEndpointStateForEndpoint(DatabaseDescriptor.getReplaceAddress()) != null - && hostId.equals(Gossiper.instance.getHostId(DatabaseDescriptor.getReplaceAddress())); - } - catch (RuntimeException ex) - { - // If a host is decomissioned and the DNS entry is removed before the - // bootstrap completes, when it completes and advertises NORMAL state to other nodes, they will be unable - // to resolve it to an InetAddress unless it happens to be cached. This could happen on nodes - // storing large amounts of data or with long index rebuild times or if new instances have been added - // to the cluster through expansion or additional host replacement. - // - // The original host replacement must have been able to resolve the replacing address on startup - // when setting StorageService.replacing, so if it is impossible to resolve now it is probably - // decommissioned and did not have the same IP address or host id. Allow the handleStateNormal - // handling to proceed, otherwise gossip state will be inconistent with some nodes believing the - // replacement host to be normal, and nodes unable to resolve the hostname will be left in JOINING. - if (ex.getCause() != null && ex.getCause().getClass() == UnknownHostException.class) - { - logger.info("Suppressed exception while checking isReplacingSameHostAddressAndHostId({}). Original host was probably decommissioned. ({})", - hostId, ex.getMessage()); - return false; - } - throw ex; // otherwise rethrow - } - } - - /** - * Handle node move to normal state. That is, node is entering token ring and participating - * in reads. - * - * @param endpoint node - */ - private void handleStateNormal(final InetAddressAndPort endpoint, final String status) - { - Collection tokens = getTokensFor(endpoint); - Set endpointsToRemove = new HashSet<>(); - - if (logger.isDebugEnabled()) - logger.debug("Node {} state {}, token {}", endpoint, status, tokens); - - if (tokenMetadata.isMember(endpoint)) - logger.info("Node {} state jump to {}", endpoint, status); - - if (tokens.isEmpty() && status.equals(VersionedValue.STATUS_NORMAL)) - logger.error("Node {} is in state normal but it has no tokens, state: {}", - endpoint, - Gossiper.instance.getEndpointStateForEndpoint(endpoint)); - - Optional replacingNode = tokenMetadata.getReplacingNode(endpoint); - if (replacingNode.isPresent()) - { - assert !endpoint.equals(replacingNode.get()) : "Pending replacement endpoint with same address is not supported"; - logger.info("Node {} will complete replacement of {} for tokens {}", endpoint, replacingNode.get(), tokens); - if (FailureDetector.instance.isAlive(replacingNode.get())) - { - logger.error("Node {} cannot complete replacement of alive node {}.", endpoint, replacingNode.get()); - return; - } - endpointsToRemove.add(replacingNode.get()); - } - - Optional replacementNode = tokenMetadata.getReplacementNode(endpoint); - if (replacementNode.isPresent()) - { - logger.warn("Node {} is currently being replaced by node {}.", endpoint, replacementNode.get()); - } - - updatePeerInfo(endpoint); - // Order Matters, TM.updateHostID() should be called before TM.updateNormalToken(), (see CASSANDRA-4300). - UUID hostId = Gossiper.instance.getHostId(endpoint); - InetAddressAndPort existing = tokenMetadata.getEndpointForHostId(hostId); - if (replacing && isReplacingSameHostAddressAndHostId(hostId)) - { - logger.warn("Not updating token metadata for {} because I am replacing it", endpoint); - } - else - { - if (existing != null && !existing.equals(endpoint)) - { - if (existing.equals(FBUtilities.getBroadcastAddressAndPort())) - { - logger.warn("Not updating host ID {} for {} because it's mine", hostId, endpoint); - tokenMetadata.removeEndpoint(endpoint); - endpointsToRemove.add(endpoint); - } - else if (Gossiper.instance.compareEndpointStartup(endpoint, existing) > 0) - { - logger.warn("Host ID collision for {} between {} and {}; {} is the new owner", hostId, existing, endpoint, endpoint); - tokenMetadata.removeEndpoint(existing); - endpointsToRemove.add(existing); - tokenMetadata.updateHostId(hostId, endpoint); - } - else - { - logger.warn("Host ID collision for {} between {} and {}; ignored {}", hostId, existing, endpoint, endpoint); - tokenMetadata.removeEndpoint(endpoint); - endpointsToRemove.add(endpoint); - } - } - else - tokenMetadata.updateHostId(hostId, endpoint); - } - - // capture because updateNormalTokens clears moving and member status - boolean isMember = tokenMetadata.isMember(endpoint); - boolean isMoving = tokenMetadata.isMoving(endpoint); - - updateTokenMetadata(endpoint, tokens, endpointsToRemove); - - if (isMoving || operationMode == Mode.MOVING) - { - tokenMetadata.removeFromMoving(endpoint); - // The above may change the local ownership. - invalidateLocalRanges(); - notifyMoved(endpoint); - } - else if (!isMember) // prior to this, the node was not a member - { - notifyJoined(endpoint); - } - - PendingRangeCalculatorService.instance.update(); - } - - /** - * Handle node preparing to leave the ring - * - * @param endpoint node - */ - private void handleStateLeaving(InetAddressAndPort endpoint) - { - // If the node is previously unknown or tokens do not match, update tokenmetadata to - // have this node as 'normal' (it must have been using this token before the - // leave). This way we'll get pending ranges right. - - ensureUpToDateTokenMetadata(VersionedValue.STATUS_LEAVING, endpoint); - - // at this point the endpoint is certainly a member with this token, so let's proceed - // normally - tokenMetadata.addLeavingEndpoint(endpoint); - PendingRangeCalculatorService.instance.update(); - } - - /** - * Handle node leaving the ring. This will happen when a node is decommissioned - * - * @param endpoint If reason for leaving is decommission, endpoint is the leaving node. - * @param pieces STATE_LEFT,token - */ - private void handleStateLeft(InetAddressAndPort endpoint, String[] pieces) - { - assert pieces.length >= 2; - Collection tokens = getTokensFor(endpoint); - - if (logger.isDebugEnabled()) - logger.debug("Node {} state left, tokens {}", endpoint, tokens); - - excise(tokens, endpoint, extractExpireTime(pieces)); - } - - /** - * Handle node moving inside the ring. - * - * @param endpoint moving endpoint address - * @param pieces STATE_MOVING, token - */ - private void handleStateMoving(InetAddressAndPort endpoint, String[] pieces) - { - ensureUpToDateTokenMetadata(VersionedValue.STATUS_MOVING, endpoint); - - assert pieces.length >= 2; - Token token = getTokenFactory().fromString(pieces[1]); - - if (logger.isDebugEnabled()) - logger.debug("Node {} state moving, new token {}", endpoint, token); - - tokenMetadata.addMovingEndpoint(token, endpoint); - - PendingRangeCalculatorService.instance.update(); - } - - /** - * Handle notification that a node being actively removed from the ring via 'removenode' - * - * @param endpoint node - * @param pieces either REMOVED_TOKEN (node is gone) or REMOVING_TOKEN (replicas need to be restored) - */ - private void handleStateRemoving(InetAddressAndPort endpoint, String[] pieces) - { - assert (pieces.length > 0); - - if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) - { - logger.info("Received removenode gossip about myself. Is this node rejoining after an explicit removenode?"); - try - { - drain(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - return; - } - if (tokenMetadata.isMember(endpoint)) - { - String state = pieces[0]; - Collection removeTokens = tokenMetadata.getTokens(endpoint); - - if (VersionedValue.REMOVED_TOKEN.equals(state)) - { - excise(removeTokens, endpoint, extractExpireTime(pieces)); - } - else if (VersionedValue.REMOVING_TOKEN.equals(state)) - { - ensureUpToDateTokenMetadata(state, endpoint); - - if (logger.isDebugEnabled()) - logger.debug("Tokens {} removed manually (endpoint was {})", removeTokens, endpoint); - - // Note that the endpoint is being removed - tokenMetadata.addLeavingEndpoint(endpoint); - PendingRangeCalculatorService.instance.update(); - - // find the endpoint coordinating this removal that we need to notify when we're done - String[] coordinator = splitValue(Gossiper.instance.getEndpointStateForEndpoint(endpoint).getApplicationState(ApplicationState.REMOVAL_COORDINATOR)); - UUID hostId = UUID.fromString(coordinator[1]); - // grab any data we are now responsible for and notify responsible node - restoreReplicaCount(endpoint, tokenMetadata.getEndpointForHostId(hostId)); - } - } - else // now that the gossiper has told us about this nonexistent member, notify the gossiper to remove it - { - if (VersionedValue.REMOVED_TOKEN.equals(pieces[0])) - addExpireTimeIfFound(endpoint, extractExpireTime(pieces)); - removeEndpoint(endpoint); - } - } - - private void excise(Collection tokens, InetAddressAndPort endpoint) - { - logger.info("Removing tokens {} for {}", tokens, endpoint); - - UUID hostId = tokenMetadata.getHostId(endpoint); - if (hostId != null && tokenMetadata.isMember(endpoint)) - { - // enough time for writes to expire and MessagingService timeout reporter callback to fire, which is where - // hints are mostly written from - using getMinRpcTimeout() / 2 for the interval. - long delay = DatabaseDescriptor.getMinRpcTimeout(MILLISECONDS) + DatabaseDescriptor.getWriteRpcTimeout(MILLISECONDS); - ScheduledExecutors.optionalTasks.schedule(() -> HintsService.instance.excise(hostId), delay, MILLISECONDS); - } - - removeEndpoint(endpoint); - tokenMetadata.removeEndpoint(endpoint); - if (!tokens.isEmpty()) - tokenMetadata.removeBootstrapTokens(tokens); - notifyLeft(endpoint); - PendingRangeCalculatorService.instance.update(); - } - - private void excise(Collection tokens, InetAddressAndPort endpoint, long expireTime) - { - addExpireTimeIfFound(endpoint, expireTime); - excise(tokens, endpoint); - } - - /** unlike excise we just need this endpoint gone without going through any notifications **/ - private void removeEndpoint(InetAddressAndPort endpoint) - { - Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.removeEndpoint(endpoint)); - SystemKeyspace.removeEndpoint(endpoint); - } - - protected void addExpireTimeIfFound(InetAddressAndPort endpoint, long expireTime) - { - if (expireTime != 0L) - { - Gossiper.instance.addExpireTimeForEndpoint(endpoint, expireTime); - } - } - - protected long extractExpireTime(String[] pieces) - { - return Long.parseLong(pieces[2]); - } - - /** - * Finds living endpoints responsible for the given ranges - * - * @param keyspaceName the keyspace ranges belong to - * @param leavingReplicas the ranges to find sources for - * @return multimap of addresses to ranges the address is responsible for - */ - private Multimap getNewSourceReplicas(String keyspaceName, Set leavingReplicas) - { - InetAddressAndPort myAddress = FBUtilities.getBroadcastAddressAndPort(); - EndpointsByRange rangeReplicas = Keyspace.open(keyspaceName).getReplicationStrategy().getRangeAddresses(tokenMetadata.cloneOnlyTokenMap()); - Multimap sourceRanges = HashMultimap.create(); - IFailureDetector failureDetector = FailureDetector.instance; - - logger.debug("Getting new source replicas for {}", leavingReplicas); - - // find alive sources for our new ranges - for (LeavingReplica leaver : leavingReplicas) - { - //We need this to find the replicas from before leaving to supply the data - Replica leavingReplica = leaver.leavingReplica; - //We need this to know what to fetch and what the transient status is - Replica ourReplica = leaver.ourReplica; - //If we are going to be a full replica only consider full replicas - Predicate replicaFilter = ourReplica.isFull() ? Replica::isFull : Predicates.alwaysTrue(); - Predicate notSelf = replica -> !replica.endpoint().equals(myAddress); - EndpointsForRange possibleReplicas = rangeReplicas.get(leavingReplica.range()); - logger.info("Possible replicas for newReplica {} are {}", ourReplica, possibleReplicas); - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - EndpointsForRange sortedPossibleReplicas = snitch.sortedByProximity(myAddress, possibleReplicas); - logger.info("Sorted possible replicas starts as {}", sortedPossibleReplicas); - Optional myCurrentReplica = tryFind(possibleReplicas, replica -> replica.endpoint().equals(myAddress)).toJavaUtil(); - - boolean transientToFull = myCurrentReplica.isPresent() && myCurrentReplica.get().isTransient() && ourReplica.isFull(); - assert !sortedPossibleReplicas.endpoints().contains(myAddress) || transientToFull : String.format("My address %s, sortedPossibleReplicas %s, myCurrentReplica %s, myNewReplica %s", myAddress, sortedPossibleReplicas, myCurrentReplica, ourReplica); - - //Originally this didn't log if it couldn't restore replication and that seems wrong - boolean foundLiveReplica = false; - for (Replica possibleReplica : sortedPossibleReplicas.filter(Predicates.and(replicaFilter, notSelf))) - { - if (failureDetector.isAlive(possibleReplica.endpoint())) - { - foundLiveReplica = true; - sourceRanges.put(possibleReplica.endpoint(), new FetchReplica(ourReplica, possibleReplica)); - break; - } - else - { - logger.debug("Skipping down replica {}", possibleReplica); - } - } - if (!foundLiveReplica) - { - logger.warn("Didn't find live replica to restore replication for " + ourReplica); - } - } - return sourceRanges; - } - - /** - * Sends a notification to a node indicating we have finished replicating data. - * - * @param remote node to send notification to - */ - private void sendReplicationNotification(InetAddressAndPort remote) - { - // notify the remote token - Message msg = Message.out(REPLICATION_DONE_REQ, noPayload); - IFailureDetector failureDetector = FailureDetector.instance; - if (logger.isDebugEnabled()) - logger.debug("Notifying {} of replication completion\n", remote); - while (failureDetector.isAlive(remote)) - { - AsyncOneResponse ior = new AsyncOneResponse(); - MessagingService.instance().sendWithCallback(msg, remote, ior); - - if (!ior.awaitUninterruptibly(DatabaseDescriptor.getRpcTimeout(NANOSECONDS), NANOSECONDS)) - continue; // try again if we timeout - - if (!ior.isSuccess()) - throw new AssertionError(ior.cause()); - - return; - } - } - - private static class LeavingReplica - { - //The node that is leaving - private final Replica leavingReplica; - - //Our range and transient status - private final Replica ourReplica; - - public LeavingReplica(Replica leavingReplica, Replica ourReplica) - { - Preconditions.checkNotNull(leavingReplica); - Preconditions.checkNotNull(ourReplica); - this.leavingReplica = leavingReplica; - this.ourReplica = ourReplica; - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - LeavingReplica that = (LeavingReplica) o; - - if (!leavingReplica.equals(that.leavingReplica)) return false; - return ourReplica.equals(that.ourReplica); - } - - public int hashCode() - { - int result = leavingReplica.hashCode(); - result = 31 * result + ourReplica.hashCode(); - return result; - } - - public String toString() - { - return "LeavingReplica{" + - "leavingReplica=" + leavingReplica + - ", ourReplica=" + ourReplica + - '}'; - } - } - - /** - * Called when an endpoint is removed from the ring. This function checks - * whether this node becomes responsible for new ranges as a - * consequence and streams data if needed. - * - * This is rather ineffective, but it does not matter so much - * since this is called very seldom - * - * @param endpoint the node that left - */ - private void restoreReplicaCount(InetAddressAndPort endpoint, final InetAddressAndPort notifyEndpoint) - { - Map> replicasToFetch = new HashMap<>(); - - InetAddressAndPort myAddress = FBUtilities.getBroadcastAddressAndPort(); - - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - { - logger.debug("Restoring replica count for keyspace {}", keyspaceName); - EndpointsByReplica changedReplicas = getChangedReplicasForLeaving(keyspaceName, endpoint, tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); - Set myNewReplicas = new HashSet<>(); - for (Map.Entry entry : changedReplicas.flattenEntries()) - { - Replica replica = entry.getValue(); - if (replica.endpoint().equals(myAddress)) - { - //Maybe we don't technically need to fetch transient data from somewhere - //but it's probably not a lot and it probably makes things a hair more resilient to people - //not running repair when they should. - myNewReplicas.add(new LeavingReplica(entry.getKey(), entry.getValue())); - } - } - logger.debug("Changed replicas for leaving {}, myNewReplicas {}", changedReplicas, myNewReplicas); - replicasToFetch.put(keyspaceName, getNewSourceReplicas(keyspaceName, myNewReplicas)); - } - - StreamPlan stream = new StreamPlan(StreamOperation.RESTORE_REPLICA_COUNT); - replicasToFetch.forEach((keyspaceName, sources) -> { - logger.debug("Requesting keyspace {} sources", keyspaceName); - sources.asMap().forEach((sourceAddress, fetchReplicas) -> { - logger.debug("Source and our replicas are {}", fetchReplicas); - //Remember whether this node is providing the full or transient replicas for this range. We are going - //to pass streaming the local instance of Replica for the range which doesn't tell us anything about the source - //By encoding it as two separate sets we retain this information about the source. - RangesAtEndpoint full = fetchReplicas.stream() - .filter(f -> f.remote.isFull()) - .map(f -> f.local) - .collect(RangesAtEndpoint.collector(myAddress)); - RangesAtEndpoint transientReplicas = fetchReplicas.stream() - .filter(f -> f.remote.isTransient()) - .map(f -> f.local) - .collect(RangesAtEndpoint.collector(myAddress)); - if (logger.isDebugEnabled()) - logger.debug("Requesting from {} full replicas {} transient replicas {}", sourceAddress, StringUtils.join(full, ", "), StringUtils.join(transientReplicas, ", ")); - - stream.requestRanges(sourceAddress, keyspaceName, full, transientReplicas); - }); - }); - StreamResultFuture future = stream.execute(); - future.addCallback(new FutureCallback() - { - public void onSuccess(StreamState finalState) - { - sendReplicationNotification(notifyEndpoint); - } - - public void onFailure(Throwable t) - { - logger.warn("Streaming to restore replica count failed", t); - // We still want to send the notification - sendReplicationNotification(notifyEndpoint); - } - }); - } - - /** - * This is used in three contexts, graceful decomission, and restoreReplicaCount/removeNode. - * Graceful decomission should never lose data and it's going to be important that transient data - * is streamed to at least one other node from this one for each range. - * - * For ranges this node replicates its removal should cause a new replica to be selected either as transient or full - * for every range. So I believe the current code doesn't have to do anything special because it will engage in streaming - * for every range it replicates to at least one other node and that should propagate the transient data that was here. - * When I graphed this out on paper the result of removal looked correct and there are no issues such as - * this node needing to create a full replica for a range it transiently replicates because what is created is just another - * transient replica to replace this node. - * @param keyspaceName - * @param endpoint - * @return - */ - // needs to be modified to accept either a keyspace or ARS. - static EndpointsByReplica getChangedReplicasForLeaving(String keyspaceName, InetAddressAndPort endpoint, TokenMetadata tokenMetadata, AbstractReplicationStrategy strat) - { - // First get all ranges the leaving endpoint is responsible for - RangesAtEndpoint replicas = strat.getAddressReplicas(endpoint); - - if (logger.isDebugEnabled()) - logger.debug("Node {} replicas [{}]", endpoint, StringUtils.join(replicas, ", ")); - - Map currentReplicaEndpoints = Maps.newHashMapWithExpectedSize(replicas.size()); - - // Find (for each range) all nodes that store replicas for these ranges as well - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); // don't do this in the loop! #7758 - for (Replica replica : replicas) - currentReplicaEndpoints.put(replica, strat.calculateNaturalReplicas(replica.range().right, metadata)); - - TokenMetadata temp = tokenMetadata.cloneAfterAllLeft(); - - // endpoint might or might not be 'leaving'. If it was not leaving (that is, removenode - // command was used), it is still present in temp and must be removed. - if (temp.isMember(endpoint)) - temp.removeEndpoint(endpoint); - - EndpointsByReplica.Builder changedRanges = new EndpointsByReplica.Builder(); - - // Go through the ranges and for each range check who will be - // storing replicas for these ranges when the leaving endpoint - // is gone. Whoever is present in newReplicaEndpoints list, but - // not in the currentReplicaEndpoints list, will be needing the - // range. - for (Replica replica : replicas) - { - EndpointsForRange newReplicaEndpoints = strat.calculateNaturalReplicas(replica.range().right, temp); - newReplicaEndpoints = newReplicaEndpoints.filter(newReplica -> { - Optional currentReplicaOptional = - tryFind(currentReplicaEndpoints.get(replica), - currentReplica -> newReplica.endpoint().equals(currentReplica.endpoint()) - ).toJavaUtil(); - //If it is newly replicating then yes we must do something to get the data there - if (!currentReplicaOptional.isPresent()) - return true; - - Replica currentReplica = currentReplicaOptional.get(); - //This transition requires streaming to occur - //Full -> transient is handled by nodetool cleanup - //transient -> transient and full -> full don't require any action - if (currentReplica.isTransient() && newReplica.isFull()) - return true; - return false; - }); - - if (logger.isDebugEnabled()) - if (newReplicaEndpoints.isEmpty()) - logger.debug("Replica {} already in all replicas", replica); - else - logger.debug("Replica {} will be responsibility of {}", replica, StringUtils.join(newReplicaEndpoints, ", ")); - changedRanges.putAll(replica, newReplicaEndpoints, Conflict.NONE); - } - - return changedRanges.build(); - } - - public void onJoin(InetAddressAndPort endpoint, EndpointState epState) { // Explicitly process STATUS or STATUS_WITH_PORT before the other @@ -3746,16 +2194,10 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public void onAlive(InetAddressAndPort endpoint, EndpointState state) { - if (tokenMetadata.isMember(endpoint)) + if (ClusterMetadata.current().directory.allAddresses().contains(endpoint)) notifyUp(endpoint); } - public void onRemove(InetAddressAndPort endpoint) - { - tokenMetadata.removeEndpoint(endpoint); - PendingRangeCalculatorService.instance.update(); - } - public void onDead(InetAddressAndPort endpoint, EndpointState state) { // interrupt any outbound connection; if the node is failing and we cannot reconnect, @@ -3807,7 +2249,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE map.put(entry.getKey().getHostAddress(withPort), FileUtils.stringifyFileSize(entry.getValue())); } // gossiper doesn't see its own updates, so we need to special-case the local node - map.put(FBUtilities.getBroadcastAddressAndPort().getHostAddress(withPort), getLoadString()); + map.put(getBroadcastAddressAndPort().getHostAddress(withPort), getLoadString()); return map; } @@ -3827,20 +2269,28 @@ public class StorageService extends NotificationBroadcasterSupport implements IE @Nullable public InetAddressAndPort getEndpointForHostId(UUID hostId) { - return tokenMetadata.getEndpointForHostId(hostId); + Directory directory = ClusterMetadata.current().directory; + NodeId nodeId = NodeId.isValidNodeId(hostId) + ? NodeId.fromUUID(hostId) + : directory.nodeIdFromHostId(hostId); + + return nodeId != null ? ClusterMetadata.current().directory.endpoint(nodeId) : null; } @Nullable public UUID getHostIdForEndpoint(InetAddressAndPort address) { - return tokenMetadata.getHostId(address); + NodeId nodeId = ClusterMetadata.current().directory.peerId(address); + if (nodeId != null) + return nodeId.toUUID(); + return null; } /* These methods belong to the MBean interface */ public List getTokens() { - return getTokens(FBUtilities.getBroadcastAddressAndPort()); + return getTokens(getBroadcastAddressAndPort()); } public List getTokens(String endpoint) throws UnknownHostException @@ -3851,7 +2301,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private List getTokens(InetAddressAndPort endpoint) { List strTokens = new ArrayList<>(); - for (Token tok : getTokenMetadata().getTokens(endpoint)) + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(endpoint); + for (Token tok : metadata.tokenMap.tokens(nodeId)) strTokens.add(tok.toString()); return strTokens; } @@ -3883,15 +2335,26 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } /** @deprecated See CASSANDRA-7544 */ + @Deprecated(since = "4.0") + public Set endpointsWithState(NodeState ... state) + { + Set states = Sets.newHashSet(state); + ClusterMetadata metadata = ClusterMetadata.current(); + return metadata.directory.states.entrySet().stream() + .filter(e -> states.contains(e.getValue())) + .map(e -> metadata.directory.endpoint(e.getKey())) + .collect(toSet()); + } + @Deprecated(since = "4.0") public List getLeavingNodes() { - return stringify(tokenMetadata.getLeavingEndpoints(), false); + return stringify(endpointsWithState(NodeState.LEAVING), false); } public List getLeavingNodesWithPort() { - return stringify(tokenMetadata.getLeavingEndpoints(), true); + return stringify(endpointsWithState(NodeState.LEAVING), true); } /** @deprecated See CASSANDRA-7544 */ @@ -3900,9 +2363,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE { List endpoints = new ArrayList<>(); - for (Pair node : tokenMetadata.getMovingEndpoints()) + for (InetAddressAndPort endpoint : endpointsWithState(MOVING)) { - endpoints.add(node.right.getAddress().getHostAddress()); + endpoints.add(endpoint.getAddress().getHostAddress()); } return endpoints; @@ -3912,9 +2375,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE { List endpoints = new ArrayList<>(); - for (Pair node : tokenMetadata.getMovingEndpoints()) + for (InetAddressAndPort endpoint : endpointsWithState(MOVING)) { - endpoints.add(node.right.getHostAddressAndPort()); + endpoints.add(endpoint.getHostAddressAndPort()); } return endpoints; @@ -3924,12 +2387,16 @@ public class StorageService extends NotificationBroadcasterSupport implements IE @Deprecated(since = "4.0") public List getJoiningNodes() { - return stringify(tokenMetadata.getBootstrapTokens().valueSet(), false); + return stringify(Iterables.concat(endpointsWithState(BOOTSTRAPPING), + endpointsWithState(BOOT_REPLACING)), + false); } public List getJoiningNodesWithPort() { - return stringify(tokenMetadata.getBootstrapTokens().valueSet(), true); + return stringify(Iterables.concat(endpointsWithState(BOOTSTRAPPING), + endpointsWithState(BOOT_REPLACING)), + true); } /** @deprecated See CASSANDRA-7544 */ @@ -3961,7 +2428,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE continue; } - if (tokenMetadata.isMember(ep)) + if (ClusterMetadata.current().directory.allAddresses().contains(ep)) ret.add(ep); } return ret; @@ -4028,7 +2495,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public int getCurrentGenerationNumber() { - return Gossiper.instance.getCurrentGenerationNumber(FBUtilities.getBroadcastAddressAndPort()); + return Gossiper.instance.getCurrentGenerationNumber(getBroadcastAddressAndPort()); } public int forceKeyspaceCleanup(String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException @@ -4038,12 +2505,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public int forceKeyspaceCleanup(int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { - if (SchemaConstants.isLocalSystemKeyspace(keyspaceName)) + if (isLocalSystemKeyspace(keyspaceName)) throw new RuntimeException("Cleanup of the system keyspace is neither necessary nor wise"); - if (tokenMetadata.getPendingRanges(keyspaceName, getBroadcastAddressAndPort()).size() > 0) - throw new RuntimeException("Node is involved in cluster membership changes. Not safe to run cleanup."); - CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL; logger.info("Starting {} on {}.{}", OperationType.CLEANUP, keyspaceName, Arrays.toString(tableNames)); for (ColumnFamilyStore cfStore : getValidColumnFamilies(false, false, keyspaceName, tableNames)) @@ -4156,12 +2620,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return statements; } - public void dropPreparedStatements(boolean memoryOnly) - { - QueryProcessor.instance.clearPreparedStatements(memoryOnly); - } - - public void forceKeyspaceCompaction(boolean splitOutput, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException { for (ColumnFamilyStore cfStore : getValidColumnFamilies(true, false, keyspaceName, tableNames)) @@ -4340,7 +2798,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ private void takeSnapshot(String tag, boolean skipFlush, DurationSpec.IntSecondsBound ttl, String... keyspaceNames) throws IOException { - if (operationMode == Mode.JOINING) + if (operationMode() == Mode.JOINING) throw new IOException("Cannot snapshot until bootstrap completes"); if (tag == null || tag.equals("")) throw new IOException("You must supply a snapshot name."); @@ -4398,7 +2856,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE if (keyspaceName == null) throw new IOException("You must supply a keyspace name"); - if (operationMode.equals(Mode.JOINING)) + if (operationMode() == Mode.JOINING) throw new IOException("Cannot snapshot until bootstrap completes"); if (tableName == null) @@ -4571,7 +3029,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE long total = 0; for (Keyspace keyspace : Keyspace.all()) { - if (SchemaConstants.isLocalSystemKeyspace(keyspace.getName())) + if (isLocalSystemKeyspace(keyspace.getName())) continue; for (ColumnFamilyStore cfStore : keyspace.getColumnFamilyStores()) @@ -4654,7 +3112,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public Pair> repair(String keyspace, Map repairSpec, List listeners) { - RepairOption option = RepairOption.parse(repairSpec, tokenMetadata.partitioner); + RepairOption option = RepairOption.parse(repairSpec, ClusterMetadata.current().partitioner); return repair(keyspace, option, listeners); } @@ -4702,7 +3160,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE // Break up given range to match ring layout in TokenMetadata ArrayList> repairingRange = new ArrayList<>(); - ArrayList tokens = new ArrayList<>(tokenMetadata.sortedTokens()); + ArrayList tokens = new ArrayList<>(ClusterMetadata.current().tokenMap.tokens()); if (!tokens.contains(parsedBeginToken)) { tokens.add(parsedBeginToken); @@ -4726,7 +3184,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public TokenFactory getTokenFactory() { - return tokenMetadata.partitioner.getTokenFactory(); + return ClusterMetadata.current().partitioner.getTokenFactory(); } private FutureTask createRepairTask(final int cmd, final String keyspace, final RepairOption options, List listeners) @@ -4735,7 +3193,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE { throw new IllegalArgumentException("the local data center must be part of the repair; requested " + options.getDataCenters() + " but DC is " + DatabaseDescriptor.getLocalDataCenter()); } - Set existingDatacenters = tokenMetadata.cloneOnlyTokenMap().getTopology().getDatacenterEndpoints().keys().elementSet(); + Set existingDatacenters = ClusterMetadata.current().directory.allDatacenterEndpoints().keys().elementSet(); List datacenters = new ArrayList<>(options.getDataCenters()); if (!existingDatacenters.containsAll(datacenters)) { @@ -4771,11 +3229,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } } - private void repairPaxosForTopologyChange(String reason) + public void repairPaxosForTopologyChange(String reason) { if (getSkipPaxosRepairOnTopologyChange() || !Paxos.useV2()) { - logger.info("skipping paxos repair for {}. skip_paxos_repair_on_topology_change is set, or v2 paxos variant is not being used", reason); + logger.info("skipping paxos repair for {}. skip_paxos_repair_on_topology_change is set", reason); return; } @@ -4825,7 +3283,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE if (DatabaseDescriptor.skipPaxosRepairOnTopologyChangeKeyspaces().contains(ksName)) continue; - List> ranges = getLocalAndPendingRanges(ksName); + Collection> ranges = getLocalAndPendingRanges(ksName); futures.add(ActiveRepairService.instance().repairPaxosForTopologyChange(ksName, ranges, reason)); } @@ -4838,7 +3296,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE if (table == null) return ImmediateFuture.success(null); - List> ranges = getLocalAndPendingRanges(table.keyspace); + Collection> ranges = getLocalAndPendingRanges(table.keyspace); PaxosCleanupLocalCoordinator coordinator = PaxosCleanupLocalCoordinator.createForAutoRepair(tableId, ranges); ScheduledExecutors.optionalTasks.submit(coordinator::start); return coordinator; @@ -4899,122 +3357,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE /* End of MBean interface methods */ - /** - * Get the "primary ranges" for the specified keyspace and endpoint. - * "Primary ranges" are the ranges that the node is responsible for storing replica primarily. - * The node that stores replica primarily is defined as the first node returned - * by {@link AbstractReplicationStrategy#calculateNaturalReplicas}. - * - * @param keyspace Keyspace name to check primary ranges - * @param ep endpoint we are interested in. - * @return primary ranges for the specified endpoint. - */ - public Collection> getPrimaryRangesForEndpoint(String keyspace, InetAddressAndPort ep) - { - AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); - Collection> primaryRanges = new HashSet<>(); - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); - for (Token token : metadata.sortedTokens()) - { - EndpointsForRange replicas = strategy.calculateNaturalReplicas(token, metadata); - if (replicas.size() > 0 && replicas.get(0).endpoint().equals(ep)) - { - Preconditions.checkState(replicas.get(0).isFull()); - primaryRanges.add(new Range<>(metadata.getPredecessor(token), token)); - } - } - return primaryRanges; - } - - /** - * Get the "primary ranges" within local DC for the specified keyspace and endpoint. - * - * @see #getPrimaryRangesForEndpoint(String, InetAddressAndPort) - * @param keyspace Keyspace name to check primary ranges - * @param referenceEndpoint endpoint we are interested in. - * @return primary ranges within local DC for the specified endpoint. - */ - public Collection> getPrimaryRangeForEndpointWithinDC(String keyspace, InetAddressAndPort referenceEndpoint) - { - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); - String localDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(referenceEndpoint); - Collection localDcNodes = metadata.getTopology().getDatacenterEndpoints().get(localDC); - AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); - - Collection> localDCPrimaryRanges = new HashSet<>(); - for (Token token : metadata.sortedTokens()) - { - EndpointsForRange replicas = strategy.calculateNaturalReplicas(token, metadata); - for (Replica replica : replicas) - { - if (localDcNodes.contains(replica.endpoint())) - { - if (replica.endpoint().equals(referenceEndpoint)) - { - localDCPrimaryRanges.add(new Range<>(metadata.getPredecessor(token), token)); - } - break; - } - } - } - - return localDCPrimaryRanges; - } - - public Collection> getLocalPrimaryRange() - { - return getLocalPrimaryRangeForEndpoint(FBUtilities.getBroadcastAddressAndPort()); - } - - public Collection> getLocalPrimaryRangeForEndpoint(InetAddressAndPort referenceEndpoint) - { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - TokenMetadata tokenMetadata = this.tokenMetadata.cloneOnlyTokenMap(); - if (!tokenMetadata.isMember(referenceEndpoint)) - return Collections.emptySet(); - String dc = snitch.getDatacenter(referenceEndpoint); - Set tokens = new HashSet<>(tokenMetadata.getTokens(referenceEndpoint)); - - // filter tokens to the single DC - List filteredTokens = Lists.newArrayList(); - for (Token token : tokenMetadata.sortedTokens()) - { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(token); - if (dc.equals(snitch.getDatacenter(endpoint))) - filteredTokens.add(token); - } - - return getAllRanges(filteredTokens).stream() - .filter(t -> tokens.contains(t.right)) - .collect(Collectors.toList()); - } - - /** - * Get all ranges that span the ring given a set - * of tokens. All ranges are in sorted order of - * ranges. - * @return ranges in sorted order - */ - public List> getAllRanges(List sortedTokens) - { - if (logger.isTraceEnabled()) - logger.trace("computing ranges for {}", StringUtils.join(sortedTokens, ", ")); - - if (sortedTokens.isEmpty()) - return Collections.emptyList(); - int size = sortedTokens.size(); - List> ranges = new ArrayList<>(size + 1); - for (int i = 1; i < size; ++i) - { - Range range = new Range<>(sortedTokens.get(i - 1), sortedTokens.get(i)); - ranges.add(range); - } - Range range = new Range<>(sortedTokens.get(size - 1), sortedTokens.get(0)); - ranges.add(range); - - return ranges; - } - /** * This method returns the N endpoints that are responsible for storing the * specified key i.e for replication. @@ -5060,15 +3402,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return getNaturalReplicasForToken(keyspaceName, partitionKeyToBytes(keyspaceName, cf, key)); } - public EndpointsForToken getNaturalReplicasForToken(String keyspaceName, ByteBuffer key) - { - Token token = tokenMetadata.partitioner.getToken(key); - return Keyspace.open(keyspaceName).getReplicationStrategy().getNaturalReplicasForToken(token); - } - public DecoratedKey getKeyFromPartition(String keyspaceName, String table, String partitionKey) { - return tokenMetadata.partitioner.decorateKey(partitionKeyToBytes(keyspaceName, table, partitionKey)); + return ClusterMetadata.current().partitioner.decorateKey(partitionKeyToBytes(keyspaceName, table, partitionKey)); } private static ByteBuffer partitionKeyToBytes(String keyspaceName, String cf, String key) @@ -5087,7 +3423,22 @@ public class StorageService extends NotificationBroadcasterSupport implements IE @Override public String getToken(String keyspaceName, String table, String key) { - return tokenMetadata.partitioner.getToken(partitionKeyToBytes(keyspaceName, table, key)).toString(); + return ClusterMetadata.current().partitioner.getToken(partitionKeyToBytes(keyspaceName, table, key)).toString(); + } + + public EndpointsForToken getNaturalReplicasForToken(String keyspaceName, ByteBuffer key) + { + ClusterMetadata metadata = ClusterMetadata.current(); + Token token = metadata.partitioner.getToken(key); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspaceName); + return metadata.placements.get(keyspaceMetadata.params.replication).reads.forToken(token).get(); + } + + public boolean isEndpointValidForWrite(String keyspace, Token token) + { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspace); + return keyspaceMetadata != null && metadata.placements.get(keyspaceMetadata.params.replication).writes.forToken(token).get().containsSelf(); } public void setLoggingLevel(String classQualifier, String rawLevel) throws Exception @@ -5161,213 +3512,32 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return keys; } - /** - * Broadcast leaving status and update local tokenMetadata accordingly - */ - private void startLeaving() + public void decommission(boolean force) { - DatabaseDescriptor.getSeverityDuringDecommission().ifPresent(DynamicEndpointSnitch::addSeverity); - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, valueFactory.leaving(getLocalTokens())); - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS, valueFactory.leaving(getLocalTokens())); - tokenMetadata.addLeavingEndpoint(FBUtilities.getBroadcastAddressAndPort()); - PendingRangeCalculatorService.instance.update(); + SingleNodeSequences.decommission(true, force); } - public void decommission(boolean force) throws InterruptedException + public void shutdownNetworking() { - if (operationMode == DECOMMISSIONED) - { - logger.info("This node was already decommissioned. There is no point in decommissioning it again."); - return; - } - - if (isDecommissioning()) - { - logger.info("This node is still decommissioning."); - return; - } - - TokenMetadata metadata = tokenMetadata.cloneAfterAllLeft(); - // there is no point to do this logic again once node was decommissioning but failed to do so - if (operationMode != Mode.LEAVING) - { - if (!tokenMetadata.isMember(FBUtilities.getBroadcastAddressAndPort())) - throw new UnsupportedOperationException("local node is not a member of the token ring yet"); - if (metadata.getAllEndpoints().size() < 2 && metadata.getAllEndpoints().contains(FBUtilities.getBroadcastAddressAndPort())) - throw new UnsupportedOperationException("no other normal nodes in the ring; decommission would be pointless"); - if (operationMode != Mode.NORMAL && operationMode != DECOMMISSION_FAILED) - throw new UnsupportedOperationException("Node in " + operationMode + " state; wait for status to become normal or restart"); - } - - if (!isDecommissioning.compareAndSet(false, true)) - throw new IllegalStateException("Node is still decommissioning. Check nodetool netstats or nodetool info."); - - if (logger.isDebugEnabled()) - logger.debug("DECOMMISSIONING"); - + shutdownClientServers(); + Gossiper.instance.stop(); try { - PendingRangeCalculatorService.instance.blockUntilFinished(); - - String dc = DatabaseDescriptor.getEndpointSnitch().getLocalDatacenter(); - - // If we're already decommissioning there is no point checking RF/pending ranges - if (operationMode != Mode.LEAVING) - { - int rf, numNodes; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - { - if (!force) - { - boolean notEnoughLiveNodes = false; - Keyspace keyspace = Keyspace.open(keyspaceName); - if (keyspace.getReplicationStrategy() instanceof NetworkTopologyStrategy) - { - NetworkTopologyStrategy strategy = (NetworkTopologyStrategy) keyspace.getReplicationStrategy(); - rf = strategy.getReplicationFactor(dc).allReplicas; - Collection datacenterEndpoints = metadata.getTopology().getDatacenterEndpoints().get(dc); - numNodes = datacenterEndpoints.size(); - if (numNodes <= rf && datacenterEndpoints.contains(FBUtilities.getBroadcastAddressAndPort())) - notEnoughLiveNodes = true; - } - else - { - Set allEndpoints = metadata.getAllEndpoints(); - numNodes = allEndpoints.size(); - rf = keyspace.getReplicationStrategy().getReplicationFactor().allReplicas; - if (numNodes <= rf && allEndpoints.contains(FBUtilities.getBroadcastAddressAndPort())) - notEnoughLiveNodes = true; - } - - if (notEnoughLiveNodes) - throw new UnsupportedOperationException("Not enough live nodes to maintain replication factor in keyspace " - + keyspaceName + " (RF = " + rf + ", N = " + numNodes + ")." - + " Perform a forceful decommission to ignore."); - } - // TODO: do we care about fixing transient/full self-movements here? probably - if (tokenMetadata.getPendingRanges(keyspaceName, FBUtilities.getBroadcastAddressAndPort()).size() > 0) - throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring"); - } - } - - startLeaving(); - long timeout = Math.max(RING_DELAY_MILLIS, BatchlogManager.getBatchlogTimeout()); - setMode(Mode.LEAVING, "sleeping " + timeout + " ms for batch processing and pending range setup", true); - Thread.sleep(timeout); - - unbootstrap(); - - // shutdown cql, gossip, messaging, Stage and set state to DECOMMISSIONED - - shutdownClientServers(); - Gossiper.instance.stop(); - try - { - MessagingService.instance().shutdown(); - } - catch (IOError ioe) - { - logger.info("failed to shutdown message service", ioe); - } - - Stage.shutdownNow(); - SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.DECOMMISSIONED); - setMode(DECOMMISSIONED, true); - // let op be responsible for killing the process + MessagingService.instance().shutdown(); } - catch (InterruptedException e) + catch (IOError ioe) { - setMode(DECOMMISSION_FAILED, true); - logger.error("Node interrupted while decommissioning"); - throw new RuntimeException("Node interrupted while decommissioning"); - } - catch (ExecutionException e) - { - setMode(DECOMMISSION_FAILED, true); - logger.error("Error while decommissioning node: {}", e.getCause().getMessage()); - throw new RuntimeException("Error while decommissioning node: " + e.getCause().getMessage()); - } - catch (Throwable t) - { - setMode(DECOMMISSION_FAILED, true); - logger.error("Error while decommissioning node: {}", t.getMessage()); - throw t; - } - finally - { - isDecommissioning.set(false); + logger.info("failed to shutdown message service", ioe); } + + Stage.shutdownNow(); + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.DECOMMISSIONED); + transientMode = Optional.of(Mode.DECOMMISSIONED); + logger.info("{}", Mode.DECOMMISSIONED); + // let op be responsible for killing the process } - private void leaveRing() - { - SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.NEEDS_BOOTSTRAP); - tokenMetadata.removeEndpoint(FBUtilities.getBroadcastAddressAndPort()); - PendingRangeCalculatorService.instance.update(); - - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, valueFactory.left(getLocalTokens(),Gossiper.computeExpireTime())); - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS, valueFactory.left(getLocalTokens(),Gossiper.computeExpireTime())); - int delay = Math.max(RING_DELAY_MILLIS, Gossiper.intervalInMillis * 2); - logger.info("Announcing that I have left the ring for {}ms", delay); - Uninterruptibles.sleepUninterruptibly(delay, MILLISECONDS); - } - - public Supplier> prepareUnbootstrapStreaming() - { - Map rangesToStream = new HashMap<>(); - - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - { - EndpointsByReplica rangesMM = getChangedReplicasForLeaving(keyspaceName, FBUtilities.getBroadcastAddressAndPort(), tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); - - if (logger.isDebugEnabled()) - logger.debug("Ranges needing transfer are [{}]", StringUtils.join(rangesMM.keySet(), ",")); - - rangesToStream.put(keyspaceName, rangesMM); - } - - return () -> streamRanges(rangesToStream); - } - - private void unbootstrap() throws ExecutionException, InterruptedException - { - Supplier> startStreaming = prepareUnbootstrapStreaming(); - - setMode(Mode.LEAVING, "replaying batch log and streaming data to other nodes", true); - - repairPaxosForTopologyChange("decommission"); - // Start with BatchLog replay, which may create hints but no writes since this is no longer a valid endpoint. - Future batchlogReplay = BatchlogManager.instance.startBatchlogReplay(); - Future streamSuccess = startStreaming.get(); - - // Wait for batch log to complete before streaming hints. - logger.debug("waiting for batch log processing."); - batchlogReplay.get(); - - Future hintsSuccess = ImmediateFuture.success(null); - - if (DatabaseDescriptor.getTransferHintsOnDecommission()) - { - setMode(Mode.LEAVING, "streaming hints to other nodes", true); - hintsSuccess = streamHints(); - } - else - { - setMode(Mode.LEAVING, "pausing dispatch and deleting hints", true); - DatabaseDescriptor.setHintedHandoffEnabled(false); - HintsService.instance.pauseDispatch(); - HintsService.instance.deleteAllHints(); - } - - // wait for the transfer runnables to signal the latch. - logger.debug("waiting for stream acks."); - streamSuccess.get(); - hintsSuccess.get(); - logger.debug("stream acks all received."); - leaveRing(); - } - - private Future streamHints() + public Future streamHints() { return HintsService.instance.transferHints(this::getPreferredHintsStreamTarget); } @@ -5375,7 +3545,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE private static EndpointsForRange getStreamCandidates(Collection endpoints) { endpoints = endpoints.stream() - .filter(endpoint -> FailureDetector.instance.isAlive(endpoint) && !FBUtilities.getBroadcastAddressAndPort().equals(endpoint)) + .filter(endpoint -> FailureDetector.instance.isAlive(endpoint) && !getBroadcastAddressAndPort().equals(endpoint)) .collect(Collectors.toList()); return SystemReplicas.getSystemReplicas(endpoints); @@ -5385,7 +3555,12 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ private UUID getPreferredHintsStreamTarget() { - Set endpoints = StorageService.instance.getTokenMetadata().cloneAfterAllLeft().getAllEndpoints(); + ClusterMetadata metadata = ClusterMetadata.current(); + + Set endpoints = metadata.directory.states.entrySet().stream() + .filter(e -> e.getValue() != NodeState.LEAVING) + .map(e -> metadata.directory.endpoint(e.getKey())) + .collect(toSet()); EndpointsForRange candidates = getStreamCandidates(endpoints); if (candidates.isEmpty()) @@ -5396,13 +3571,13 @@ public class StorageService extends NotificationBroadcasterSupport implements IE else { // stream to the closest peer as chosen by the snitch - candidates = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), candidates); + candidates = DatabaseDescriptor.getEndpointSnitch().sortedByProximity(getBroadcastAddressAndPort(), candidates); InetAddressAndPort hintsDestinationHost = candidates.get(0).endpoint(); - return tokenMetadata.getHostId(hintsDestinationHost); + return ClusterMetadata.current().directory.peerId(hintsDestinationHost).toUUID(); } } - public void move(String newToken) throws IOException + public void move(String newToken) { try { @@ -5410,83 +3585,9 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } catch (ConfigurationException e) { - throw new IOException(e.getMessage()); + throw new IllegalArgumentException(e.getMessage()); } - move(getTokenFactory().fromString(newToken)); - } - - /** - * move the node to new token or find a new token to boot to according to load - * - * @param newToken new token to boot to, or if null, find balanced token to boot to - * - * @throws IOException on any I/O operation error - */ - private void move(Token newToken) throws IOException - { - if (newToken == null) - throw new IOException("Can't move to the undefined (null) token."); - - if (tokenMetadata.sortedTokens().contains(newToken)) - throw new IOException("target token " + newToken + " is already owned by another node."); - - // address of the current node - InetAddressAndPort localAddress = FBUtilities.getBroadcastAddressAndPort(); - - // This doesn't make any sense in a vnodes environment. - if (getTokenMetadata().getTokens(localAddress).size() > 1) - { - logger.error("Invalid request to move(Token); This node has more than one token and cannot be moved thusly."); - throw new UnsupportedOperationException("This node has more than one token and cannot be moved thusly."); - } - - List keyspacesToProcess = ImmutableList.copyOf(Schema.instance.distributedKeyspaces().names()); - - PendingRangeCalculatorService.instance.blockUntilFinished(); - // checking if data is moving to this node - for (String keyspaceName : keyspacesToProcess) - { - // TODO: do we care about fixing transient/full self-movements here? - if (tokenMetadata.getPendingRanges(keyspaceName, localAddress).size() > 0) - throw new UnsupportedOperationException("data is currently moving to this node; unable to leave the ring"); - } - - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, valueFactory.moving(newToken)); - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS, valueFactory.moving(newToken)); - setMode(Mode.MOVING, String.format("Moving %s from %s to %s.", localAddress, getLocalTokens().iterator().next(), newToken), true); - - setMode(Mode.MOVING, String.format("Sleeping %s ms before start streaming/fetching ranges", RING_DELAY_MILLIS), true); - Uninterruptibles.sleepUninterruptibly(RING_DELAY_MILLIS, MILLISECONDS); - - RangeRelocator relocator = new RangeRelocator(Collections.singleton(newToken), keyspacesToProcess, tokenMetadata); - relocator.calculateToFromStreams(); - - repairPaxosForTopologyChange("move"); - if (relocator.streamsNeeded()) - { - setMode(Mode.MOVING, "fetching new ranges and streaming old ranges", true); - try - { - relocator.stream().get(); - } - catch (InterruptedException e) - { - throw new UncheckedInterruptedException(e); - } - catch (ExecutionException e) - { - throw new RuntimeException("Interrupted while waiting for stream/fetch ranges to finish: " + e.getMessage()); - } - } - else - { - setMode(Mode.MOVING, "No ranges to fetch/stream", true); - } - - setTokens(Collections.singleton(newToken)); // setting new token as we have everything settled - - if (logger.isDebugEnabled()) - logger.debug("Successfully moved to new token {}", getLocalTokens().iterator().next()); + SingleNodeSequences.move(getTokenFactory().fromString(newToken)); } public String getRemovalStatus() @@ -5504,24 +3605,26 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ private String getRemovalStatus(boolean withPort) { - if (removingNode == null) + ClusterMetadata metadata = ClusterMetadata.current(); + StringBuilder sb = new StringBuilder(); + boolean found = false; + for (Map.Entry stateEntry : metadata.directory.states.entrySet()) { - return "No token removals in process."; - } - - Collection toFormat = replicatingNodes; - if (!withPort) - { - toFormat = new ArrayList(replicatingNodes.size()); - for (InetAddressAndPort node : replicatingNodes) + NodeId nodeId = stateEntry.getKey(); + NodeState state = stateEntry.getValue(); + if (state == NodeState.LEAVING) { - toFormat.add(node.toString(false)); + MultiStepOperation seq = metadata.inProgressSequences.get(nodeId); + if (seq != null && seq.kind() == MultiStepOperation.Kind.REMOVE) + { + sb.append("Removing node ").append(nodeId).append(" (").append(metadata.directory.endpoint(nodeId)).append(')').append(": ").append(seq.status()); + found = true; + } } } - - return String.format("Removing token (%s). Waiting for replication confirmation from [%s].", - tokenMetadata.getTokens(removingNode).iterator().next(), - StringUtils.join(toFormat, ",")); + if (!found) + sb.append("No removals in progress."); + return sb.toString(); } /** @@ -5531,22 +3634,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ public void forceRemoveCompletion() { - if (!replicatingNodes.isEmpty() || tokenMetadata.getSizeOfLeavingEndpoints() > 0) - { - logger.warn("Removal not confirmed for for {}", StringUtils.join(this.replicatingNodes, ",")); - for (InetAddressAndPort endpoint : tokenMetadata.getLeavingEndpoints()) - { - UUID hostId = tokenMetadata.getHostId(endpoint); - Gossiper.instance.advertiseTokenRemoved(endpoint, hostId); - excise(tokenMetadata.getTokens(endpoint), endpoint); - } - replicatingNodes.clear(); - removingNode = null; - } - else - { - logger.warn("No nodes to force removal on, call 'removenode' first"); - } + throw new IllegalStateException("Can't force remove completion, abort the remove operation and retry."); } /** @@ -5560,76 +3648,26 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ public void removeNode(String hostIdString) { - InetAddressAndPort myAddress = FBUtilities.getBroadcastAddressAndPort(); - UUID localHostId = tokenMetadata.getHostId(myAddress); - UUID hostId = UUID.fromString(hostIdString); - InetAddressAndPort endpoint = tokenMetadata.getEndpointForHostId(hostId); + removeNode(hostIdString, false); + } - if (endpoint == null) - throw new UnsupportedOperationException("Host ID not found."); + public void removeNode(String hostIdString, boolean force) + { + NodeId toRemove = NodeId.fromString(hostIdString); + SingleNodeSequences.removeNode(toRemove, force); + } - if (!tokenMetadata.isMember(endpoint)) - throw new UnsupportedOperationException("Node to be removed is not a member of the token ring"); - - if (endpoint.equals(myAddress)) - throw new UnsupportedOperationException("Cannot remove self"); - - if (Gossiper.instance.getLiveMembers().contains(endpoint)) - throw new UnsupportedOperationException("Node " + endpoint + " is alive and owns this ID. Use decommission command to remove it from the ring"); - - // A leaving endpoint that is dead is already being removed. - if (tokenMetadata.isLeaving(endpoint)) - logger.warn("Node {} is already being removed, continuing removal anyway", endpoint); - - if (!replicatingNodes.isEmpty()) - throw new UnsupportedOperationException("This node is already processing a removal. Wait for it to complete, or use 'removenode force' if this has failed."); - - Collection tokens = tokenMetadata.getTokens(endpoint); - - // Find the endpoints that are going to become responsible for data - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) + public void assassinateEndpoint(String address) + { + try { - // if the replication factor is 1 the data is lost so we shouldn't wait for confirmation - if (Keyspace.open(keyspaceName).getReplicationStrategy().getReplicationFactor().allReplicas == 1) - continue; - - // get all ranges that change ownership (that is, a node needs - // to take responsibility for new range) - EndpointsByReplica changedRanges = getChangedReplicasForLeaving(keyspaceName, endpoint, tokenMetadata, Keyspace.open(keyspaceName).getReplicationStrategy()); - IFailureDetector failureDetector = FailureDetector.instance; - for (InetAddressAndPort ep : transform(changedRanges.flattenValues(), Replica::endpoint)) - { - if (failureDetector.isAlive(ep)) - replicatingNodes.add(ep); - else - logger.warn("Endpoint {} is down and will not receive data for re-replication of {}", ep, endpoint); - } + InetAddressAndPort endpoint = InetAddressAndPort.getByName(address); + Assassinate.assassinateEndpoint(endpoint); } - removingNode = endpoint; - - tokenMetadata.addLeavingEndpoint(endpoint); - PendingRangeCalculatorService.instance.update(); - - // the gossiper will handle spoofing this node's state to REMOVING_TOKEN for us - // we add our own token so other nodes to let us know when they're done - Gossiper.instance.advertiseRemoving(endpoint, hostId, localHostId); - - // kick off streaming commands - restoreReplicaCount(endpoint, myAddress); - - // wait for ReplicationDoneVerbHandler to signal we're done - while (!replicatingNodes.isEmpty()) + catch (UnknownHostException e) { - Uninterruptibles.sleepUninterruptibly(100, MILLISECONDS); + throw new RuntimeException("Could not assassinate an unresolvable endpoint"); } - - excise(tokens, endpoint); - - // gossiper will indicate the token has left - Gossiper.instance.advertiseTokenRemoved(endpoint, hostId); - - replicatingNodes.clear(); - removingNode = null; } public void confirmReplication(InetAddressAndPort node) @@ -5647,39 +3685,83 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } } + public void markDecommissionFailed() + { + logger.info(DECOMMISSION_FAILED.toString()); + transientMode = Optional.of(DECOMMISSION_FAILED); + } + + public void markBootstrapFailed() + { + logger.info(JOINING_FAILED.toString()); + transientMode = Optional.of(JOINING_FAILED); + } + + /* + - Use system_views.local to get information about the node (todo: we might still need a jmx endpoint for that since you can't run cql queries on drained etc nodes) + */ + @Deprecated(since = "CEP-21") public String getOperationMode() { - return operationMode.toString(); + return operationMode().toString(); + } + + public Mode operationMode() + { + if (!isInitialized()) + return Mode.STARTING; + + if (transientMode.isPresent()) + return transientMode.get(); + + NodeState nodeState = ClusterMetadata.current().myNodeState(); + switch (nodeState) + { + case REGISTERED: + return Mode.STARTING; + case BOOT_REPLACING: + case BOOTSTRAPPING: + return Mode.JOINING; + case JOINED: + return NORMAL; + case LEAVING: + return Mode.LEAVING; + case LEFT: + return Mode.DECOMMISSIONED; + case MOVING: + return Mode.MOVING; + } + throw new IllegalStateException("Bad node state: " + nodeState); } public boolean isStarting() { - return operationMode == Mode.STARTING; + return operationMode() == Mode.STARTING; } public boolean isMoving() { - return operationMode == Mode.MOVING; + return operationMode() == Mode.MOVING; } public boolean isJoining() { - return operationMode == Mode.JOINING; + return operationMode() == Mode.JOINING; } public boolean isDrained() { - return operationMode == Mode.DRAINED; + return operationMode() == Mode.DRAINED; } public boolean isDraining() { - return operationMode == Mode.DRAINING; + return operationMode() == Mode.DRAINING; } public boolean isNormal() { - return operationMode == Mode.NORMAL; + return operationMode() == NORMAL; } public boolean isDecommissioned() @@ -5689,17 +3771,22 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public boolean isDecommissionFailed() { - return operationMode == DECOMMISSION_FAILED; + return operationMode() == DECOMMISSION_FAILED; } public boolean isDecommissioning() { - return isDecommissioning.get(); + return operationMode == Mode.LEAVING || operationMode == DECOMMISSION_FAILED; } public boolean isBootstrapFailed() { - return operationMode == JOINING_FAILED; + return operationMode() == JOINING_FAILED; + } + + public void clearTransientMode() + { + transientMode = Optional.empty(); } public String getDrainProgress() @@ -5733,7 +3820,12 @@ public class StorageService extends NotificationBroadcasterSupport implements IE try { - setMode(Mode.DRAINING, "starting drain process", !isFinalShutdown); + String msg = "starting drain process"; + if (!isFinalShutdown) + logger.info(msg); + else + logger.debug(msg); + transientMode = Optional.of(Mode.DRAINING); try { @@ -5755,7 +3847,10 @@ public class StorageService extends NotificationBroadcasterSupport implements IE ActiveRepairService.instance().stop(); if (!isFinalShutdown) - setMode(Mode.DRAINING, "shutting down MessageService", false); + { + logger.debug("shutting down MessageService"); + transientMode = Optional.of(Mode.DRAINING); + } // In-progress writes originating here could generate hints to be written, // which is currently scheduled on the mutation stage. So shut down MessagingService @@ -5772,14 +3867,20 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } if (!isFinalShutdown) - setMode(Mode.DRAINING, "clearing mutation stage", false); + { + logger.debug("clearing mutation stage"); + transientMode = Optional.of(Mode.DRAINING); + } Stage.shutdownAndAwaitMutatingExecutors(false, DRAIN_EXECUTOR_TIMEOUT_MS.getInt(), TimeUnit.MILLISECONDS); StorageProxy.instance.verifyNoHintsInProgress(); if (!isFinalShutdown) - setMode(Mode.DRAINING, "flushing column families", false); + { + logger.debug("flushing column families"); + transientMode = Optional.of(Mode.DRAINING); + } // we don't want to start any new compactions while we are draining disableAutoCompaction(); @@ -5856,7 +3957,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } finally { - setMode(Mode.DRAINED, !isFinalShutdown); + if (!isFinalShutdown) + logger.info("{}", Mode.DRAINED); + else + logger.debug("{}", Mode.DRAINED); + transientMode = Optional.of(Mode.DRAINED); } } catch (Throwable t) @@ -5937,7 +4042,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE if (isDraining()) // when draining isShutdown is also true, so we check first to return a more accurate message throw new IllegalStateException(String.format("Unable to start %s because the node is draining.", service)); - if (isShutdown()) // do not rely on operationMode in case it gets changed to decomissioned or other + if (isShutdown()) // do not rely on operationMode in case it gets changed to decommissioned or other throw new IllegalStateException(String.format("Unable to start %s because the node was drained.", service)); if (!isNormal() && joinRing) // if the node is not joining the ring, it is gossipping-only member which is in STARTING state forever @@ -5949,18 +4054,10 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public IPartitioner setPartitionerUnsafe(IPartitioner newPartitioner) { IPartitioner oldPartitioner = DatabaseDescriptor.setPartitionerUnsafe(newPartitioner); - tokenMetadata = tokenMetadata.cloneWithNewPartitioner(newPartitioner); valueFactory = new VersionedValue.VersionedValueFactory(newPartitioner); return oldPartitioner; } - TokenMetadata setTokenMetadataUnsafe(TokenMetadata tmd) - { - TokenMetadata old = tokenMetadata; - tokenMetadata = tmd; - return old; - } - public void truncate(String keyspace, String table) throws TimeoutException, IOException { verifyKeyspaceIsValid(keyspace); @@ -5977,13 +4074,15 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public Map getOwnership() { - List sortedTokens = tokenMetadata.sortedTokens(); + ClusterMetadata metadata = ClusterMetadata.current(); + List sortedTokens = metadata.tokenMap.tokens(); // describeOwnership returns tokens in an unspecified order, let's re-order them - Map tokenMap = new TreeMap(tokenMetadata.partitioner.describeOwnership(sortedTokens)); + Map tokenMap = new TreeMap<>(metadata.tokenMap.partitioner().describeOwnership(sortedTokens)); Map nodeMap = new LinkedHashMap<>(); for (Map.Entry entry : tokenMap.entrySet()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(entry.getKey()); + NodeId nodeId = metadata.tokenMap.owner(entry.getKey()); + InetAddressAndPort endpoint = metadata.directory.endpoint(nodeId); Float tokenOwnership = entry.getValue(); if (nodeMap.containsKey(endpoint.getAddress())) nodeMap.put(endpoint.getAddress(), nodeMap.get(endpoint.getAddress()) + tokenOwnership); @@ -5995,13 +4094,15 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public Map getOwnershipWithPort() { - List sortedTokens = tokenMetadata.sortedTokens(); + ClusterMetadata metadata = ClusterMetadata.current(); + List sortedTokens = metadata.tokenMap.tokens(); // describeOwnership returns tokens in an unspecified order, let's re-order them - Map tokenMap = new TreeMap(tokenMetadata.partitioner.describeOwnership(sortedTokens)); + Map tokenMap = new TreeMap(metadata.tokenMap.partitioner().describeOwnership(sortedTokens)); Map nodeMap = new LinkedHashMap<>(); for (Map.Entry entry : tokenMap.entrySet()) { - InetAddressAndPort endpoint = tokenMetadata.getEndpoint(entry.getKey()); + NodeId nodeId = metadata.tokenMap.owner(entry.getKey()); + InetAddressAndPort endpoint = metadata.directory.endpoint(nodeId); Float tokenOwnership = entry.getValue(); if (nodeMap.containsKey(endpoint.toString())) nodeMap.put(endpoint.toString(), nodeMap.get(endpoint.toString()) + tokenOwnership); @@ -6022,22 +4123,31 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ private LinkedHashMap getEffectiveOwnership(String keyspace) { + ClusterMetadata metadata = ClusterMetadata.current(); + ReplicationParams replicationParams = null; AbstractReplicationStrategy strategy; if (keyspace != null) { - Keyspace keyspaceInstance = Schema.instance.getKeyspaceInstance(keyspace); + if (isLocalSystemKeyspace(keyspace)) + throw new IllegalStateException("Ownership values for keyspaces with LocalStrategy are meaningless"); + + KeyspaceMetadata keyspaceInstance = metadata.schema.getKeyspaces().getNullable(keyspace); if (keyspaceInstance == null) throw new IllegalArgumentException("The keyspace " + keyspace + ", does not exist"); - if (keyspaceInstance.getReplicationStrategy() instanceof LocalStrategy) + if (keyspaceInstance.replicationStrategy instanceof LocalStrategy) throw new IllegalStateException("Ownership values for keyspaces with LocalStrategy are meaningless"); - strategy = keyspaceInstance.getReplicationStrategy(); + + strategy = keyspaceInstance.replicationStrategy; + replicationParams = keyspaceInstance.params.replication; } else { - Collection userKeyspaces = Schema.instance.getUserKeyspaces(); + Set userKeyspaces = metadata.schema.getKeyspaces() + .without(SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES) + .names(); - if (!userKeyspaces.isEmpty()) + if (userKeyspaces.size() > 0) { keyspace = userKeyspaces.iterator().next(); AbstractReplicationStrategy replicationStrategy = Schema.instance.getKeyspaceInstance(keyspace).getReplicationStrategy(); @@ -6047,7 +4157,8 @@ public class StorageService extends NotificationBroadcasterSupport implements IE throw new IllegalStateException("Non-system keyspaces don't have the same replication settings, effective ownership information is meaningless"); } } - else + + if (keyspace == null) { keyspace = "system_traces"; } @@ -6055,21 +4166,29 @@ public class StorageService extends NotificationBroadcasterSupport implements IE Keyspace keyspaceInstance = Schema.instance.getKeyspaceInstance(keyspace); if (keyspaceInstance == null) throw new IllegalStateException("The node does not have " + keyspace + " yet, probably still bootstrapping. Effective ownership information is meaningless."); + replicationParams = keyspaceInstance.getMetadata().params.replication; strategy = keyspaceInstance.getReplicationStrategy(); } - TokenMetadata metadata = tokenMetadata.cloneOnlyTokenMap(); + if (replicationParams.isMeta()) + { + LinkedHashMap ownership = Maps.newLinkedHashMap(); + metadata.placements.get(replicationParams).writes.byEndpoint().flattenValues().forEach((r) -> { + ownership.put(r.endpoint(), 1.0f); + }); + return ownership; + } Collection> endpointsGroupedByDc = new ArrayList<>(); // mapping of dc's to nodes, use sorted map so that we get dcs sorted - SortedMap> sortedDcsToEndpoints = new TreeMap<>(metadata.getTopology().getDatacenterEndpoints().asMap()); + SortedMap> sortedDcsToEndpoints = new TreeMap<>(ClusterMetadata.current().directory.allDatacenterEndpoints().asMap()); for (Collection endpoints : sortedDcsToEndpoints.values()) endpointsGroupedByDc.add(endpoints); - Map tokenOwnership = tokenMetadata.partitioner.describeOwnership(tokenMetadata.sortedTokens()); + Map tokenOwnership = metadata.partitioner.describeOwnership(metadata.tokenMap.tokens()); LinkedHashMap finalOwnership = Maps.newLinkedHashMap(); - RangesByEndpoint endpointToRanges = strategy.getAddressReplicas(); + RangesByEndpoint endpointToRanges = strategy.getAddressReplicas(metadata); // calculate ownership per dc for (Collection endpoints : endpointsGroupedByDc) { @@ -6122,13 +4241,13 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public Map getViewBuildStatuses(String keyspace, String view, boolean withPort) { Map coreViewStatus = SystemDistributedKeyspace.viewStatus(keyspace, view); - Map hostIdToEndpoint = tokenMetadata.getEndpointToHostIdMapForReading(); + Map hostIdToEndpoint = ClusterMetadata.current().directory.addresses; Map result = new HashMap<>(); - for (Map.Entry entry : hostIdToEndpoint.entrySet()) + for (Map.Entry entry : hostIdToEndpoint.entrySet()) { - UUID hostId = entry.getValue(); - InetAddressAndPort endpoint = entry.getKey(); + UUID hostId = entry.getKey().toUUID(); + InetAddressAndPort endpoint = entry.getValue().broadcastAddress; result.put(endpoint.toString(withPort), coreViewStatus.getOrDefault(hostId, "UNKNOWN")); } @@ -6211,10 +4330,6 @@ public class StorageService extends NotificationBroadcasterSupport implements IE // point snitch references to the new instance DatabaseDescriptor.setEndpointSnitch(newSnitch); - for (String ks : Schema.instance.getKeyspaces()) - { - Keyspace.open(ks).getReplicationStrategy().snitch = newSnitch; - } } else { @@ -6228,71 +4343,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE snitch.applyConfigChanges(); } } - - updateTopology(); } - /** - * Send data to the endpoints that will be responsible for it in the future - * - * @param rangesToStreamByKeyspace keyspaces and data ranges with endpoints included for each - * @return async Future for whether stream was success - */ - private Future streamRanges(Map rangesToStreamByKeyspace) + public StreamStateStore streamStateStore() { - // First, we build a list of ranges to stream to each host, per table - Map sessionsToStreamByKeyspace = new HashMap<>(); - - for (Map.Entry entry : rangesToStreamByKeyspace.entrySet()) - { - String keyspace = entry.getKey(); - EndpointsByReplica rangesWithEndpoints = entry.getValue(); - - if (rangesWithEndpoints.isEmpty()) - continue; - - //Description is always Unbootstrap? Is that right? - Map>> transferredRangePerKeyspace = SystemKeyspace.getTransferredRanges("Unbootstrap", - keyspace, - StorageService.instance.getTokenMetadata().partitioner); - RangesByEndpoint.Builder replicasPerEndpoint = new RangesByEndpoint.Builder(); - for (Map.Entry endPointEntry : rangesWithEndpoints.flattenEntries()) - { - Replica local = endPointEntry.getKey(); - Replica remote = endPointEntry.getValue(); - Set> transferredRanges = transferredRangePerKeyspace.get(remote.endpoint()); - if (transferredRanges != null && transferredRanges.contains(local.range())) - { - logger.debug("Skipping transferred range {} of keyspace {}, endpoint {}", local, keyspace, remote); - continue; - } - - replicasPerEndpoint.put(remote.endpoint(), remote.decorateSubrange(local.range())); - } - - sessionsToStreamByKeyspace.put(keyspace, replicasPerEndpoint.build()); - } - - StreamPlan streamPlan = new StreamPlan(StreamOperation.DECOMMISSION); - - // Vinculate StreamStateStore to current StreamPlan to update transferred rangeas per StreamSession - streamPlan.listeners(streamStateStore); - - for (Map.Entry entry : sessionsToStreamByKeyspace.entrySet()) - { - String keyspaceName = entry.getKey(); - RangesByEndpoint replicasPerEndpoint = entry.getValue(); - - for (Map.Entry rangesEntry : replicasPerEndpoint.asMap().entrySet()) - { - RangesAtEndpoint replicas = rangesEntry.getValue(); - InetAddressAndPort newEndpoint = rangesEntry.getKey(); - - // TODO each call to transferRanges re-flushes, this is potentially a lot of waste - streamPlan.transferRanges(newEndpoint, keyspaceName, replicas); - } - } - return streamPlan.execute(); + return streamStateStore; } public void bulkLoad(String directory) @@ -6328,7 +4383,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE this.keyspace = keyspace; try { - for (Map.Entry, EndpointsForRange> entry : StorageService.instance.getRangeToAddressMap(keyspace).entrySet()) + for (Map.Entry, EndpointsForRange> entry : getRangeToAddressMap(keyspace).entrySet()) { Range range = entry.getKey(); EndpointsForRange replicas = entry.getValue(); @@ -6377,7 +4432,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE List keys = new ArrayList<>(); for (Keyspace keyspace : Keyspace.nonLocalStrategy()) { - for (Range range : getPrimaryRangesForEndpoint(keyspace.getName(), FBUtilities.getBroadcastAddressAndPort())) + for (Range range : getPrimaryRangesForEndpoint(keyspace.getName(), getBroadcastAddressAndPort())) keys.addAll(keySamples(keyspace.getColumnFamilyStores(), range)); } @@ -6434,21 +4489,21 @@ public class StorageService extends NotificationBroadcasterSupport implements IE @Override // Note from parent javadoc: ks and table are nullable public boolean startSamplingPartitions(String ks, String table, int duration, int interval, int capacity, int count, List samplers) { - Preconditions.checkArgument(duration > 0, "Sampling duration %s must be positive.", duration); + checkArgument(duration > 0, "Sampling duration %s must be positive.", duration); - Preconditions.checkArgument(interval <= 0 || interval >= duration, + checkArgument(interval <= 0 || interval >= duration, "Sampling interval %s should be greater then or equals to duration %s if defined.", interval, duration); - Preconditions.checkArgument(capacity > 0 && capacity <= 1024, + checkArgument(capacity > 0 && capacity <= 1024, "Sampling capacity %s must be positive and the max value is 1024 (inclusive).", capacity); - Preconditions.checkArgument(count > 0 && count < capacity, + checkArgument(count > 0 && count < capacity, "Sampling count %s must be positive and smaller than capacity %s.", count, capacity); - Preconditions.checkArgument(!samplers.isEmpty(), "Samplers cannot be empty."); + checkArgument(!samplers.isEmpty(), "Samplers cannot be empty."); Set available = EnumSet.allOf(Sampler.SamplerType.class); samplers.forEach((x) -> checkArgument(available.contains(Sampler.SamplerType.valueOf(x)), @@ -6481,12 +4536,14 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public void resetLocalSchema() throws IOException { - Schema.instance.resetLocalSchema(); + // TODO: remove method? + //Schema.instance.resetLocalSchema(); } public void reloadLocalSchema() { - Schema.instance.reloadSchemaAndAnnounceVersion(); + // TODO: remove method? + //Schema.instance.reloadSchema(); } public void setTraceProbability(double probability) @@ -6970,19 +5027,17 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } @Override + @Deprecated(since = "4.0") public Map> getOutstandingSchemaVersions() { - Map> outstanding = Schema.instance.getOutstandingSchemaVersions(); - return outstanding.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), - e -> e.getValue().stream().map(InetSocketAddress::getAddress).collect(Collectors.toSet()))); + throw new RuntimeException("Deprecated"); } @Override + @Deprecated(since = "CEP-21") public Map> getOutstandingSchemaVersionsWithPort() { - Map> outstanding = Schema.instance.getOutstandingSchemaVersions(); - return outstanding.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), - e -> e.getValue().stream().map(Object::toString).collect(Collectors.toSet()))); + throw new RuntimeException("Deprecated"); } public boolean autoOptimiseIncRepairStreams() @@ -7179,7 +5234,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE public boolean getSkipPaxosRepairOnTopologyChange() { - return DatabaseDescriptor.skipPaxosRepairOnTopologyChange(); + return true;//TODO //DatabaseDescriptor.skipPaxosRepairOnTopologyChange(); } public void setSkipPaxosRepairOnTopologyChange(boolean v) @@ -7393,6 +5448,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE DatabaseDescriptor.setMinTrackedPartitionTombstoneCount(value); } + @Override public void setSkipStreamDiskSpaceCheck(boolean value) { if (value != DatabaseDescriptor.getSkipStreamDiskSpaceCheck()) @@ -7400,6 +5456,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE DatabaseDescriptor.setSkipStreamDiskSpaceCheck(value); } + @Override public boolean getSkipStreamDiskSpaceCheck() { return DatabaseDescriptor.getSkipStreamDiskSpaceCheck(); diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java index 4ca8194164..d160d2f5e6 100644 --- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -536,6 +536,9 @@ public interface StorageServiceMBean extends NotificationEmitter * enpoint that had it) from the ring */ public void removeNode(String token); + public void removeNode(String token, boolean force); + + public void assassinateEndpoint(String addr); /** * Get the status of a token removal. @@ -1060,6 +1063,7 @@ public interface StorageServiceMBean extends NotificationEmitter public boolean resumeBootstrap(); public String getBootstrapState(); + void abortBootstrap(String nodeId, String endpoint); /** Gets the concurrency settings for processing stages*/ static class StageConcurrency implements Serializable @@ -1155,6 +1159,7 @@ public interface StorageServiceMBean extends NotificationEmitter */ @Deprecated(since = "4.0") public Map> getOutstandingSchemaVersions(); + @Deprecated(since = "CEP-21") public Map> getOutstandingSchemaVersionsWithPort(); // see CASSANDRA-3200 @@ -1259,4 +1264,4 @@ public interface StorageServiceMBean extends NotificationEmitter public void setSkipStreamDiskSpaceCheck(boolean value); public boolean getSkipStreamDiskSpaceCheck(); -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/service/WriteResponseHandler.java b/src/java/org/apache/cassandra/service/WriteResponseHandler.java index 6fe9e527cd..657bba86c9 100644 --- a/src/java/org/apache/cassandra/service/WriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/WriteResponseHandler.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.net.Message; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.utils.FBUtilities; /** * Handles blocking writes for ONE, ANY, TWO, THREE, QUORUM, and ALL consistency levels. @@ -56,6 +57,7 @@ public class WriteResponseHandler extends AbstractWriteResponseHandler public void onResponse(Message m) { + replicaPlan.collectSuccess(m == null ? FBUtilities.getBroadcastAddressAndPort() : m.from()); if (responsesUpdater.decrementAndGet(this) == 0) signal(); //Must be last after all subclass processing diff --git a/src/java/org/apache/cassandra/service/disk/usage/DiskUsageBroadcaster.java b/src/java/org/apache/cassandra/service/disk/usage/DiskUsageBroadcaster.java index 4504ac7f62..30da32b314 100644 --- a/src/java/org/apache/cassandra/service/disk/usage/DiskUsageBroadcaster.java +++ b/src/java/org/apache/cassandra/service/disk/usage/DiskUsageBroadcaster.java @@ -54,6 +54,7 @@ public class DiskUsageBroadcaster implements IEndpointStateChangeSubscriber public DiskUsageBroadcaster(DiskUsageMonitor monitor) { this.monitor = monitor; + // TODO: switch to TCM? Gossiper.instance.register(this); } diff --git a/src/java/org/apache/cassandra/service/paxos/Paxos.java b/src/java/org/apache/cassandra/service/paxos/Paxos.java index 3b87ffb545..99570ba917 100644 --- a/src/java/org/apache/cassandra/service/paxos/Paxos.java +++ b/src/java/org/apache/cassandra/service/paxos/Paxos.java @@ -49,6 +49,8 @@ import org.apache.cassandra.locator.ReplicaLayout; import org.apache.cassandra.locator.ReplicaLayout.ForTokenWrite; import org.apache.cassandra.locator.ReplicaPlan.ForRead; import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; @@ -85,9 +87,12 @@ import org.apache.cassandra.service.CASRequest; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.FailureRecordingCallback.AsMap; import org.apache.cassandra.service.paxos.Commit.Proposal; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.service.reads.DataResolver; import org.apache.cassandra.service.reads.repair.NoopReadRepair; import org.apache.cassandra.service.paxos.cleanup.PaxosTableRepairs; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.triggers.TriggerExecutor; import org.apache.cassandra.utils.CassandraVersion; @@ -105,7 +110,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.PAXOS_MODE import static org.apache.cassandra.config.Config.PaxosVariant.v2_without_linearizable_reads_or_rejected_writes; import static org.apache.cassandra.db.Keyspace.openAndGetStore; import static org.apache.cassandra.exceptions.RequestFailureReason.TIMEOUT; -import static org.apache.cassandra.gms.ApplicationState.RELEASE_VERSION; import static org.apache.cassandra.config.DatabaseDescriptor.*; import static org.apache.cassandra.db.ConsistencyLevel.*; import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer; @@ -314,7 +318,7 @@ public class Paxos /** * Encapsulates the peers we will talk to for this operation. */ - static class Participants implements ForRead, Supplier + static class Participants implements ForRead { final Keyspace keyspace; @@ -355,8 +359,13 @@ public class Paxos */ final int sizeOfReadQuorum; - Participants(Keyspace keyspace, ConsistencyLevel consistencyForConsensus, ReplicaLayout.ForTokenWrite all, ReplicaLayout.ForTokenWrite electorate, EndpointsForToken live) + final Epoch epoch; + final Function recompute; + + Participants(Epoch epoch, Keyspace keyspace, ConsistencyLevel consistencyForConsensus, ReplicaLayout.ForTokenWrite all, ReplicaLayout.ForTokenWrite electorate, EndpointsForToken live, + Function recompute) { + this.epoch = epoch; this.keyspace = keyspace; this.replicationStrategy = all.replicationStrategy(); this.consistencyForConsensus = consistencyForConsensus; @@ -369,6 +378,7 @@ public class Paxos this.allLive = live; this.sizeOfReadQuorum = electorate.natural().size() / 2 + 1; this.sizeOfConsensusQuorum = sizeOfReadQuorum + electorate.pending().size(); + this.recompute = recompute; } @Override @@ -384,16 +394,43 @@ public class Paxos return electorateNatural; } - static Participants get(TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus) + @Override + public boolean stillAppliesTo(ClusterMetadata newMetadata) { - Keyspace keyspace = Keyspace.open(table.keyspace); - ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspace, token); + if (newMetadata.epoch.equals(epoch)) + return true; + + Participants newParticipants = recompute.apply(newMetadata); + return newParticipants.electorate.equals(electorate); + } + + @Override + public void collectSuccess(InetAddressAndPort inetAddressAndPort) + { + + } + + @Override + public void collectFailure(InetAddressAndPort inetAddressAndPort, RequestFailureReason t) + { + + } + + static Participants get(ClusterMetadata metadata, TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus) + { + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); + ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, token); ReplicaLayout.ForTokenWrite electorate = consistencyForConsensus.isDatacenterLocal() ? all.filter(InOurDc.replicas()) : all; EndpointsForToken live = all.all().filter(FailureDetector.isReplicaAlive); + return new Participants(metadata.epoch, Keyspace.open(table.keyspace), consistencyForConsensus, all, electorate, live, + (cm) -> get(cm, table, token, consistencyForConsensus)); + } - return new Participants(keyspace, consistencyForConsensus, all, electorate, live); + static Participants get(TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus) + { + return get(ClusterMetadata.current(), table, token, consistencyForConsensus); } static Participants get(TableMetadata cfm, DecoratedKey key, ConsistencyLevel consistency) @@ -416,7 +453,7 @@ public class Paxos if (sizeOfConsensusQuorum > sizeOfPoll()) { mark(isWrite, m -> m.unavailables, consistencyForConsensus); - throw new UnavailableException("Cannot achieve consistency level " + consistencyForConsensus, consistencyForConsensus, sizeOfConsensusQuorum, sizeOfPoll()); + throw new UnavailableException("Cannot achieve consistency level " + consistencyForConsensus + " " + sizeOfConsensusQuorum + " > " + sizeOfPoll(), consistencyForConsensus, sizeOfConsensusQuorum, sizeOfPoll()); } } @@ -441,10 +478,9 @@ public class Paxos return electorateLive.anyMatch(Paxos::isOldParticipant); } - @Override - public Participants get() + public Epoch epoch() { - return this; + return epoch; } @Override @@ -638,13 +674,13 @@ public class Paxos return cas(key, request, consistencyForConsensus, consistencyForCommit, clientState, nanoTime(), proposeDeadline, commitDeadline); } private static RowIterator cas(DecoratedKey partitionKey, - CASRequest request, - ConsistencyLevel consistencyForConsensus, - ConsistencyLevel consistencyForCommit, - ClientState clientState, - long start, - long proposeDeadline, - long commitDeadline + CASRequest request, + ConsistencyLevel consistencyForConsensus, + ConsistencyLevel consistencyForCommit, + ClientState clientState, + long start, + long proposeDeadline, + long commitDeadline ) throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException { @@ -847,7 +883,9 @@ public class Paxos switch (PAXOS_VARIANT) { - default: throw new AssertionError(); + default: + if (!read.metadata().keyspace.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) + throw new AssertionError(); case v2_without_linearizable_reads_or_rejected_writes: case v2_without_linearizable_reads: @@ -1013,7 +1051,6 @@ public class Paxos // we don't need to test the side effects, as we just want to start again, and fall through // to the superseded section below prepare = new PaxosPrepare.Superseded(proposeResult.superseded().by, inProgress.participants); - } } @@ -1033,7 +1070,8 @@ public class Paxos // round's proposal (if any). PaxosPrepare.Success success = prepare.success(); - DataResolver resolver = new DataResolver(query, success.participants, NoopReadRepair.instance, query.creationTimeNanos()); + Supplier plan = () -> success.participants; + DataResolver resolver = new DataResolver<>(query, plan, NoopReadRepair.instance, query.creationTimeNanos()); for (int i = 0 ; i < success.responses.size() ; ++i) resolver.preprocess(success.responses.get(i)); @@ -1081,7 +1119,7 @@ public class Paxos public static boolean isInRangeAndShouldProcess(InetAddressAndPort from, DecoratedKey key, TableMetadata table, boolean includesRead) { Keyspace keyspace = Keyspace.open(table.keyspace); - return (includesRead ? EndpointsForToken.natural(keyspace, key.getToken()) + return (includesRead ? EndpointsForToken.natural(keyspace, key.getToken()).get() : ReplicaLayout.forTokenWriteLiveAndDown(keyspace, key.getToken()).all() ).contains(getBroadcastAddressAndPort()); } @@ -1239,13 +1277,15 @@ public class Paxos static boolean isOldParticipant(Replica replica) { - String version = Gossiper.instance.getForEndpoint(replica.endpoint(), RELEASE_VERSION); + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId addr = metadata.directory.peerId(replica.endpoint()); + CassandraVersion version = ClusterMetadata.current().directory.version(addr).cassandraVersion; if (version == null) return false; try { - return new CassandraVersion(version).compareTo(MODERN_PAXOS_RELEASE) < 0; + return version.compareTo(MODERN_PAXOS_RELEASE) < 0; } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java index a7f58e1383..7d0c114299 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java @@ -44,6 +44,7 @@ import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.metrics.PaxosMetrics; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; @@ -51,8 +52,8 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.paxos.PaxosPrepare.Status.Outcome; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.vint.VIntCoding; @@ -63,7 +64,6 @@ import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_REQ; import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_RSP; import static org.apache.cassandra.service.paxos.Ballot.Flag.NONE; import static org.apache.cassandra.service.paxos.Commit.*; -import static org.apache.cassandra.service.paxos.Commit.CompareResult.WAS_REPROPOSED_BY; import static org.apache.cassandra.service.paxos.Paxos.*; import static org.apache.cassandra.service.paxos.PaxosPrepare.Status.Outcome.*; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -469,13 +469,14 @@ public class PaxosPrepare extends PaxosRequestCallback im Stage.GOSSIP.executor().execute(() -> { Gossiper.instance.notifyFailureDetector(permitted.gossipInfo); Gossiper.instance.applyStateLocally(permitted.gossipInfo); - // TODO: We should also wait for schema pulls/pushes, however this would be quite an involved change to MigrationManager // (which currently drops some migration tasks on the floor). // Note it would be fine for us to fail to complete the migration task and simply treat this response as a failure/timeout. // once any pending ranges have been calculated, refresh our Participants list and submit the promise - PendingRangeCalculatorService.instance.executeWhenFinished(() -> permittedOrTerminateIfElectorateMismatch(permitted, from)); + // todo: verify that this is correct, we no longer have any pending ranges, just call this immediately + // PendingRangeCalculatorService.instance.executeWhenFinished(() -> permittedOrTerminateIfElectorateMismatch(permitted, from)); + permittedOrTerminateIfElectorateMismatch(permitted, from); }); } @@ -485,7 +486,7 @@ public class PaxosPrepare extends PaxosRequestCallback im return; // if the electorate has changed, finish so we can retry with the updated view of the ring - if (!Electorate.get(request.table, request.partitionKey, consistency(request.ballot)).equals(participants.electorate)) + if (!participants.stillAppliesTo(ClusterMetadata.current())) { signalDone(ELECTORATE_MISMATCH); return; @@ -961,6 +962,7 @@ public class PaxosPrepare extends PaxosRequestCallback im @Nullable final ReadResponse readResponse; // latestAcceptedButNotCommitted and latestCommitted were the same before and after the read occurred, and no incomplete promise was witnessed final boolean hadProposalStability; + // it would be great if we could get rid of this, but probably we need to preserve for migration purposes final Map gossipInfo; @Nullable final Ballot supersededBy; @@ -1005,6 +1007,8 @@ public class PaxosPrepare extends PaxosRequestCallback im @Override public void doVerb(Message message) { + ClusterMetadataService.instance().fetchLogFromPeerOrCMSAsync(ClusterMetadata.current(), message.from(), message.epoch()); + Response response = execute(message.payload, message.from()); if (response == null) MessagingService.instance().respondWithFailure(UNKNOWN, message); diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java index 45a3664731..368c16211c 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java @@ -42,10 +42,6 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.exceptions.UnavailableException; -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.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -58,6 +54,8 @@ import org.apache.cassandra.net.RequestCallbackWithFailure; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.FBUtilities; @@ -546,7 +544,7 @@ public class PaxosRepair extends AbstractPaxosRepair */ public static boolean hasSufficientLiveNodesForTopologyChange(Keyspace keyspace, Range range, Collection liveEndpoints) { - return hasSufficientLiveNodesForTopologyChange(keyspace.getReplicationStrategy().getNaturalReplicasForToken(range.right).endpoints(), + return hasSufficientLiveNodesForTopologyChange(ClusterMetadata.current().placements.get(keyspace.getMetadata().params.replication).reads.forRange(range).endpoints(), liveEndpoints, DatabaseDescriptor.getEndpointSnitch()::getDatacenter, DatabaseDescriptor.paxoTopologyRepairNoDcChecks(), @@ -659,40 +657,21 @@ public class PaxosRepair extends AbstractPaxosRepair return (version.major == 4 && version.minor > 0) || version.major > 4; } - static String getPeerVersion(InetAddressAndPort peer) + static boolean validatePeerCompatibility(ClusterMetadata metadata, Replica peer) { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(peer); - if (epState == null) - return null; - - VersionedValue value = epState.getApplicationState(ApplicationState.RELEASE_VERSION); - if (value == null) - return null; - - try - { - return value.value; - } - catch (IllegalArgumentException e) - { - return null; - } - } - - static boolean validatePeerCompatibility(Replica peer) - { - String versionString = getPeerVersion(peer.endpoint()); - CassandraVersion version = versionString != null ? new CassandraVersion(versionString) : null; + NodeId nodeId = metadata.directory.peerId(peer.endpoint()); + CassandraVersion version = metadata.directory.version(nodeId).cassandraVersion; boolean result = validateVersionCompatibility(version); if (!result) - logger.info("PaxosRepair isn't supported by {} on version {}", peer, versionString); + logger.info("PaxosRepair isn't supported by {} on version {}", peer, version); return result; } static boolean validatePeerCompatibility(TableMetadata table, Range range) { + ClusterMetadata metadata = ClusterMetadata.current(); Participants participants = Participants.get(table, range.right, ConsistencyLevel.SERIAL); - return Iterables.all(participants.all, PaxosRepair::validatePeerCompatibility); + return Iterables.all(participants.all, (participant) -> validatePeerCompatibility(metadata, participant)); } public static boolean validatePeerCompatibility(TableMetadata table, Collection> ranges) diff --git a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanup.java b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanup.java index 6eb1ebd574..c71577d389 100644 --- a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanup.java +++ b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanup.java @@ -19,7 +19,6 @@ package org.apache.cassandra.service.paxos.cleanup; import java.util.Collection; -import java.util.List; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -36,13 +35,11 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.paxos.Ballot; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.concurrent.AsyncFuture; import org.apache.cassandra.utils.concurrent.Future; @@ -120,18 +117,7 @@ public class PaxosCleanup extends AsyncFuture implements Runnable private static boolean isOutOfRange(String ksName, Collection> repairRanges) { Keyspace keyspace = Keyspace.open(ksName); - List> localRanges = Range.normalize(keyspace.getReplicationStrategy() - .getAddressReplicas() - .get(FBUtilities.getBroadcastAddressAndPort()) - .ranges()); - - RangesAtEndpoint pendingRanges = StorageService.instance.getTokenMetadata().getPendingRanges(ksName, FBUtilities.getBroadcastAddressAndPort()); - if (!pendingRanges.isEmpty()) - { - localRanges.addAll(pendingRanges.ranges()); - localRanges = Range.normalize(localRanges); - } - + Collection> localRanges = Range.normalize(ClusterMetadata.current().localWriteRanges(keyspace.getMetadata())); for (Range repairRange : Range.normalize(repairRanges)) { if (!Iterables.any(localRanges, localRange -> localRange.contains(repairRange))) diff --git a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanupRequest.java b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanupRequest.java index 4db457f4af..33d6fbd173 100644 --- a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanupRequest.java +++ b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosCleanupRequest.java @@ -39,6 +39,8 @@ import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.UUIDSerializer; import static org.apache.cassandra.net.MessagingService.instance; @@ -73,6 +75,9 @@ public class PaxosCleanupRequest if (!PaxosCleanup.isInRangeAndShouldProcess(request.ranges, request.tableId)) { + // Try catching up, in case it's us + ClusterMetadataService.instance().fetchLogFromPeerOrCMSAsync(ClusterMetadata.current(), in.from(),in.epoch()); + String msg = String.format("Rejecting cleanup request %s from %s. Some ranges are not replicated (%s)", request.session, in.from(), request.ranges); Message response = Message.out(PAXOS2_CLEANUP_RSP2, PaxosCleanupResponse.failed(request.session, msg)); diff --git a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosStartPrepareCleanup.java b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosStartPrepareCleanup.java index 9f30692ad4..c6191f0019 100644 --- a/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosStartPrepareCleanup.java +++ b/src/java/org/apache/cassandra/service/paxos/cleanup/PaxosStartPrepareCleanup.java @@ -39,7 +39,6 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.*; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.PaxosRepairHistory; @@ -126,8 +125,6 @@ public class PaxosStartPrepareCleanup extends AsyncFuture i // (which currently drops some migration tasks on the floor). // Note it would be fine for us to fail to complete the migration task and simply treat this response as a failure/timeout. } - // even if we have th latest gossip info, wait until pending range calculations are complete - PendingRangeCalculatorService.instance.blockUntilFinished(); } public static class Request diff --git a/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java b/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java index ee5c762a1f..aa914df2d2 100644 --- a/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java +++ b/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java @@ -175,7 +175,7 @@ public class PaxosRows if (!proposalValue.hasRemaining()) return true; - return isEmpty(proposalValue, DeserializationHelper.Flag.LOCAL, key); + return isEmpty(proposalValue, DeserializationHelper.Flag.LOCAL, key, proposalVersion); } catch (IOException e) { diff --git a/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java b/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java index 744dd4d07d..b341ef351e 100644 --- a/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java +++ b/src/java/org/apache/cassandra/service/paxos/uncommitted/UncommittedTableData.java @@ -185,7 +185,7 @@ public class UncommittedTableData return Range.normalize(FULL_RANGE); String ksName = table.getKeyspaceName(); - List> ranges = StorageService.instance.getLocalAndPendingRanges(ksName); + Collection> ranges = StorageService.instance.getLocalAndPendingRanges(ksName); // don't filter anything if we're not aware of any locally replicated ranges if (ranges.isEmpty()) @@ -563,10 +563,10 @@ public class UncommittedTableData synchronized void maybeScheduleMerge() { - logger.info("Scheduling uncommitted paxos data merge task for {}.{}", keyspace(), table()); if (data.files.size() < 2 || merge != null) return; + logger.info("Scheduling uncommitted paxos data merge task for {}.{}", keyspace(), table()); createMergeTask().maybeSchedule(); } diff --git a/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java new file mode 100644 index 0000000000..c549cb4cab --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java @@ -0,0 +1,73 @@ +/* + * 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.v1; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.paxos.Commit; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.NoSpamLogger; + +public abstract class AbstractPaxosVerbHandler implements IVerbHandler +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractPaxosVerbHandler.class); + private static final String logMessageTemplate = "Received paxos request from {} for token {} outside valid range for keyspace {}"; + + public void doVerb(Message message) + { + Commit commit = message.payload; + DecoratedKey key = commit.update.partitionKey(); + if (isOutOfRangeCommit(commit.update.metadata().keyspace, key)) + { + StorageService.instance.incOutOfRangeOperationCount(); + Keyspace.open(commit.update.metadata().keyspace).metric.outOfRangeTokenPaxosRequests.inc(); + + // Log at most 1 message per second + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, message.from(), key.getToken(), commit.update.metadata().keyspace); + + sendFailureResponse(message); + } + else + { + processMessage(message); + } + } + + abstract void processMessage(Message message); + + private static void sendFailureResponse(Message respondTo) + { + Message reply = respondTo.failureResponse(RequestFailureReason.UNKNOWN); + MessagingService.instance().send(reply, respondTo.from()); + } + + private static boolean isOutOfRangeCommit(String keyspace, DecoratedKey key) + { + return ! StorageService.instance.isEndpointValidForWrite(keyspace, key.getToken()); + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java index 2ebcfeb10f..b31900ea40 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/PrepareVerbHandler.java @@ -17,14 +17,13 @@ */ package org.apache.cassandra.service.paxos.v1; -import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.PrepareResponse; -public class PrepareVerbHandler implements IVerbHandler +public class PrepareVerbHandler extends AbstractPaxosVerbHandler { public static PrepareVerbHandler instance = new PrepareVerbHandler(); @@ -33,7 +32,8 @@ public class PrepareVerbHandler implements IVerbHandler return PaxosState.legacyPrepare(toPrepare); } - public void doVerb(Message message) + @Override + public void processMessage(Message message) { Message reply = message.responseWith(doPrepare(message.payload)); MessagingService.instance().send(reply, message.from()); diff --git a/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java index 54c8c6771b..ba6b78633b 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/ProposeVerbHandler.java @@ -17,13 +17,12 @@ */ package org.apache.cassandra.service.paxos.v1; -import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.PaxosState; -public class ProposeVerbHandler implements IVerbHandler +public class ProposeVerbHandler extends AbstractPaxosVerbHandler { public static final ProposeVerbHandler instance = new ProposeVerbHandler(); @@ -32,7 +31,8 @@ public class ProposeVerbHandler implements IVerbHandler return PaxosState.legacyPropose(proposal); } - public void doVerb(Message message) + @Override + public void processMessage(Message message) { Boolean response = doPropose(message.payload); Message reply = message.responseWith(response); diff --git a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java index 8fc5d1d89b..6010d17678 100644 --- a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java +++ b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java @@ -42,6 +42,7 @@ import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.service.StorageProxy.LocalReadRunnable; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.service.reads.repair.ReadRepair; import org.apache.cassandra.tracing.TraceState; import org.apache.cassandra.tracing.Tracing; @@ -147,7 +148,7 @@ public abstract class AbstractReadExecutor traceState.trace("reading {} from {}", readCommand.isDigestQuery() ? "digest" : "data", endpoint); if (null == message) - message = readCommand.createMessage(false); + message = readCommand.createMessage(false).withEpoch(ClusterMetadata.current().epoch); MessagingService.instance().sendWithCallback(message, endpoint, handler); } @@ -181,7 +182,8 @@ public abstract class AbstractReadExecutor /** * @return an executor appropriate for the configured speculative read policy */ - public static AbstractReadExecutor getReadExecutor(SinglePartitionReadCommand command, + public static AbstractReadExecutor getReadExecutor(ClusterMetadata metadata, + SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel, long queryStartNanoTime) throws UnavailableException { @@ -189,7 +191,8 @@ public abstract class AbstractReadExecutor ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id); SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry; - ReplicaPlan.ForTokenRead replicaPlan = ReplicaPlans.forRead(keyspace, + ReplicaPlan.ForTokenRead replicaPlan = ReplicaPlans.forRead(metadata, + keyspace, command.partitionKey().getToken(), command.indexQueryPlan(), consistencyLevel, @@ -438,7 +441,7 @@ public abstract class AbstractReadExecutor logger.trace("Timed out waiting on digest mismatch repair requests"); // the caught exception here will have CL.ALL from the repair command, // not whatever CL the initial command was at (CASSANDRA-7947) - throw new ReadTimeoutException(replicaPlan().consistencyLevel(), handler.blockFor - 1, handler.blockFor, true); + throw new ReadTimeoutException(replicaPlan().consistencyLevel(), handler.replicaPlan().readQuorum() - 1, handler.replicaPlan().readQuorum(), true); } } diff --git a/src/java/org/apache/cassandra/service/reads/ReadCallback.java b/src/java/org/apache/cassandra/service/reads/ReadCallback.java index c25b1f0f02..06276bcb2c 100644 --- a/src/java/org/apache/cassandra/service/reads/ReadCallback.java +++ b/src/java/org/apache/cassandra/service/reads/ReadCallback.java @@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.MessageParams; import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.concurrent.Condition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,7 +64,6 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< public final ResponseResolver resolver; final Condition condition = newOneTimeCondition(); private final long queryStartNanoTime; - final int blockFor; // TODO: move to replica plan as well? // this uses a plain reference, but is initialised before handoff to any other threads; the later updates // may not be visible to the threads immediately, but ReplicaPlan only contains final fields, so they will never see an uninitialised object final ReplicaPlan.Shared replicaPlan; @@ -82,13 +82,12 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< this.resolver = resolver; this.queryStartNanoTime = queryStartNanoTime; this.replicaPlan = replicaPlan; - this.blockFor = replicaPlan.get().readQuorum(); this.failureReasonByEndpoint = new ConcurrentHashMap<>(); // we don't support read repair (or rapid read protection) for range scans yet (CASSANDRA-6897) - assert !(command instanceof PartitionRangeReadCommand) || blockFor >= replicaPlan().contacts().size(); + assert !(command instanceof PartitionRangeReadCommand) || replicaPlan().readQuorum() >= replicaPlan().contacts().size(); if (logger.isTraceEnabled()) - logger.trace("Blockfor is {}; setting up requests to {}", blockFor, this.replicaPlan); + logger.trace("Blockfor is {}; setting up requests to {}", replicaPlan().readQuorum(), this.replicaPlan); } protected P replicaPlan() @@ -120,7 +119,7 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< * CASSANDRA-16097 */ int received = resolver.responses.size(); - boolean failed = failures > 0 && (blockFor > received || !resolver.isDataPresent()); + boolean failed = failures > 0 && (replicaPlan().readQuorum() > received || !resolver.isDataPresent()); // If all messages came back as a TIMEOUT then signaled=true and failed=true. // Need to distinguish between a timeout and a failure (network, bad data, etc.), so store an extra field. // see CASSANDRA-17828 @@ -139,32 +138,28 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< if (!snapshot.isEmpty()) CoordinatorWarnings.update(command, snapshot); } - if (signaled && !failed) + + if (signaled && !failed && replicaPlan().stillAppliesTo(ClusterMetadata.current())) return; if (isTracing()) { String gotData = received > 0 ? (resolver.isDataPresent() ? " (including data)" : " (only digests)") : ""; - Tracing.trace("{}; received {} of {} responses{}", !timedout ? "Failed" : "Timed out", received, blockFor, gotData); + Tracing.trace("{}; received {} of {} responses{}", !timedout ? "Failed" : "Timed out", received, replicaPlan().readQuorum(), gotData); } else if (logger.isDebugEnabled()) { String gotData = received > 0 ? (resolver.isDataPresent() ? " (including data)" : " (only digests)") : ""; - logger.debug("{}; received {} of {} responses{}", !timedout ? "Failed" : "Timed out", received, blockFor, gotData); + logger.debug("{}; received {} of {} responses{}", !timedout ? "Failed" : "Timed out", received, replicaPlan().readQuorum(), gotData); } if (snapshot != null) - snapshot.maybeAbort(command, replicaPlan().consistencyLevel(), received, blockFor, resolver.isDataPresent(), failureReasonByEndpoint); + snapshot.maybeAbort(command, replicaPlan().consistencyLevel(), received, replicaPlan().readQuorum(), resolver.isDataPresent(), failureReasonByEndpoint); // Same as for writes, see AbstractWriteResponseHandler throw !timedout - ? new ReadFailureException(replicaPlan().consistencyLevel(), received, blockFor, resolver.isDataPresent(), failureReasonByEndpoint) - : new ReadTimeoutException(replicaPlan().consistencyLevel(), received, blockFor, resolver.isDataPresent()); - } - - public int blockFor() - { - return blockFor; + ? new ReadFailureException(replicaPlan().consistencyLevel(), received, replicaPlan().readQuorum(), resolver.isDataPresent(), failureReasonByEndpoint) + : new ReadTimeoutException(replicaPlan().consistencyLevel(), received, replicaPlan().readQuorum(), resolver.isDataPresent()); } @Override @@ -176,6 +171,7 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< if (WarningContext.isSupported(params.keySet())) { RequestFailureReason reason = getWarningContext().updateCounters(params, from); + replicaPlan().collectFailure(message.from(), reason); if (reason != null) { onFailure(message.from(), reason); @@ -183,6 +179,7 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< } } resolver.preprocess(message); + replicaPlan().collectSuccess(message.from()); /* * Ensure that data is present and the response accumulator has properly published the @@ -190,7 +187,7 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< * the minimum number of required results, but it guarantees at least the minimum will * be accessible when we do signal. (see CASSANDRA-16807) */ - if (resolver.isDataPresent() && resolver.responses.size() >= blockFor) + if (resolver.isDataPresent() && resolver.responses.size() >= replicaPlan().readQuorum()) condition.signalAll(); } @@ -229,7 +226,7 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< failureReasonByEndpoint.put(from, failureReason); - if (blockFor + failuresUpdater.incrementAndGet(this) > replicaPlan().contacts().size()) + if (replicaPlan().readQuorum() + failuresUpdater.incrementAndGet(this) > replicaPlan().contacts().size()) condition.signalAll(); } diff --git a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java index 67815ac925..e3e19a8804 100644 --- a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java @@ -529,11 +529,10 @@ public class ReplicaFilteringProtection> filter); ReplicaPlan.ForTokenRead replicaPlan = ReplicaPlans.forSingleReplicaRead(keyspace, key.getToken(), source); - ReplicaPlan.SharedForTokenRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); try { - return executeReadCommand(cmd, source, sharedReplicaPlan); + return executeReadCommand(cmd, source, ReplicaPlan.shared(replicaPlan)); } catch (ReadTimeoutException e) { diff --git a/src/java/org/apache/cassandra/service/reads/ResponseResolver.java b/src/java/org/apache/cassandra/service/reads/ResponseResolver.java index 02e565d536..8e6b6d803d 100644 --- a/src/java/org/apache/cassandra/service/reads/ResponseResolver.java +++ b/src/java/org/apache/cassandra/service/reads/ResponseResolver.java @@ -34,7 +34,6 @@ public abstract class ResponseResolver, P extends Replica protected static final Logger logger = LoggerFactory.getLogger(ResponseResolver.class); protected final ReadCommand command; - // TODO: this doesn't need to be a full ReplicaPlan; just a replica collection protected final Supplier replicaPlan; // Accumulator gives us non-blocking thread-safety with optimal algorithmic constraints diff --git a/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java b/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java index c14dc3c4c8..b159218ac4 100644 --- a/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/ReplicaPlanIterator.java @@ -37,8 +37,8 @@ import org.apache.cassandra.index.Index; import org.apache.cassandra.locator.LocalStrategy; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.locator.ReplicaPlans; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.Pair; @@ -96,11 +96,11 @@ class ReplicaPlanIterator extends AbstractIterator return Collections.singletonList(queryRange); } - TokenMetadata tokenMetadata = StorageService.instance.getTokenMetadata(); + ClusterMetadata metadata = ClusterMetadata.current(); List> ranges = new ArrayList<>(); // divide the queryRange into pieces delimited by the ring and minimum tokens - Iterator ringIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), queryRange.left.getToken(), true); + Iterator ringIter = TokenRingUtils.ringIterator(metadata.tokenMap.tokens(), queryRange.left.getToken(), true); AbstractBounds remainder = queryRange; while (ringIter.hasNext()) { diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java index ebb417de27..0d2ac069d5 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java @@ -61,35 +61,36 @@ public class BlockingPartitionRepair extends AsyncFuture implements RequestCallback { private final DecoratedKey key; - private final ReplicaPlan.ForWrite writePlan; + private final ReplicaPlan.ForWrite repairPlan; private final Map pendingRepairs; private final CountDownLatch latch; private final Predicate shouldBlockOn; - + private final int blockFor; private volatile long mutationsSentTime; - public BlockingPartitionRepair(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite writePlan) + public BlockingPartitionRepair(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite repairPlan) { - this(key, repairs, writePlan, - writePlan.consistencyLevel().isDatacenterLocal() ? InOurDc.endpoints() : Predicates.alwaysTrue()); + this(key, repairs, repairPlan, + repairPlan.consistencyLevel().isDatacenterLocal() ? InOurDc.endpoints() : Predicates.alwaysTrue()); } - public BlockingPartitionRepair(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite writePlan, Predicate shouldBlockOn) + + @VisibleForTesting + public BlockingPartitionRepair(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite repairPlan, Predicate shouldBlockOn) { this.key = key; this.pendingRepairs = new ConcurrentHashMap<>(repairs); - this.writePlan = writePlan; - this.shouldBlockOn = shouldBlockOn; - - int blockFor = writePlan.writeQuorum(); - // here we remove empty repair mutations from the block for total, since - // we're not sending them mutations - for (Replica participant : writePlan.contacts()) + // Remove empty repair mutations from the block for total, since we're not sending them. + // Besides, remote dcs can sometimes get involved in dc-local reads. We want to repair them if they do, but we + // they shouldn't block for them. + int adjustedBlockFor = repairPlan.writeQuorum(); + for (Replica participant : repairPlan.contacts()) { - // remote dcs can sometimes get involved in dc-local reads. We want to repair - // them if they do, but they shouldn't interfere with blocking the client read. if (!repairs.containsKey(participant) && shouldBlockOn.test(participant.endpoint())) - blockFor--; + adjustedBlockFor--; } + this.blockFor = adjustedBlockFor; + this.repairPlan = repairPlan; + this.shouldBlockOn = shouldBlockOn; // there are some cases where logically identical data can return different digests // For read repair, this would result in ReadRepairHandler being called with a map of @@ -99,15 +100,20 @@ public class BlockingPartitionRepair latch = newCountDownLatch(Math.max(blockFor, 0)); } + public ReplicaPlan.ForWrite repairPlan() + { + return repairPlan; + } + int blockFor() { - return writePlan.writeQuorum(); + return blockFor; } @VisibleForTesting int waitingOn() { - return (int) latch.count(); + return latch.count(); } @VisibleForTesting @@ -115,7 +121,7 @@ public class BlockingPartitionRepair { if (shouldBlockOn.test(from)) { - pendingRepairs.remove(writePlan.lookup(from)); + pendingRepairs.remove(repairPlan.lookup(from)); latch.decrement(); } } @@ -123,6 +129,7 @@ public class BlockingPartitionRepair @Override public void onResponse(Message msg) { + repairPlan.collectSuccess(msg.from()); ack(msg.from()); } @@ -166,6 +173,7 @@ public class BlockingPartitionRepair if (!shouldBlockOn.test(destination.endpoint())) pendingRepairs.remove(destination); + ReadRepairDiagnostics.sendInitialRepair(this, destination.endpoint(), mutation); } } @@ -208,7 +216,7 @@ public class BlockingPartitionRepair if (awaitRepairsUntil(timeout + timeoutUnit.convert(mutationsSentTime, TimeUnit.NANOSECONDS), timeoutUnit)) return; - EndpointsForToken newCandidates = writePlan.liveUncontacted(); + EndpointsForToken newCandidates = repairPlan.liveUncontacted(); if (newCandidates.isEmpty()) return; @@ -230,7 +238,7 @@ public class BlockingPartitionRepair if (mutation == null) { - mutation = BlockingReadRepairs.createRepairMutation(update, writePlan.consistencyLevel(), replica.endpoint(), true); + mutation = BlockingReadRepairs.createRepairMutation(update, repairPlan.consistencyLevel(), replica.endpoint(), true); versionedMutations[versionIdx] = mutation; } @@ -249,7 +257,7 @@ public class BlockingPartitionRepair Keyspace getKeyspace() { - return writePlan.keyspace(); + return repairPlan.keyspace(); } DecoratedKey getKey() @@ -259,6 +267,6 @@ public class BlockingPartitionRepair ConsistencyLevel getConsistency() { - return writePlan.consistencyLevel(); + return repairPlan.consistencyLevel(); } } diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java index 9143a475fe..334d9f7f12 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java @@ -36,6 +36,7 @@ import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.metrics.ReadRepairMetrics; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tracing.Tracing; import static java.util.concurrent.TimeUnit.MICROSECONDS; @@ -82,6 +83,8 @@ public class BlockingReadRepair, P extends ReplicaPlan.Fo public void awaitWrites() { BlockingPartitionRepair timedOut = null; + ReplicaPlan.ForWrite repairPlan = null; + for (BlockingPartitionRepair repair : repairs) { if (!repair.awaitRepairsUntil(DatabaseDescriptor.getReadRpcTimeout(NANOSECONDS) + queryStartNanoTime, NANOSECONDS)) @@ -89,6 +92,7 @@ public class BlockingReadRepair, P extends ReplicaPlan.Fo timedOut = repair; break; } + repairPlan = repair.repairPlan(); } if (timedOut != null) { @@ -103,6 +107,9 @@ public class BlockingReadRepair, P extends ReplicaPlan.Fo throw new ReadTimeoutException(replicaPlan().consistencyLevel(), received, blockFor, true); } + + if (repairs.isEmpty() || repairPlan.stillAppliesTo(ClusterMetadata.current())) + return; } @Override diff --git a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java index 079080ab6f..7677c264ea 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java +++ b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java @@ -51,7 +51,6 @@ import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; -import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.schema.ColumnMetadata; public class RowIteratorMergeListener> @@ -69,7 +68,7 @@ public class RowIteratorMergeListener> private final Row.Builder[] currentRows; private final RowDiffListener diffListener; private final ReplicaPlan.ForRead readPlan; - private final ReplicaPlan.ForWrite writePlan; + private final ReplicaPlan.ForWrite repairPlan; // The partition level deletion for the merge row. private DeletionTime partitionLevelDeletion; @@ -88,7 +87,10 @@ public class RowIteratorMergeListener> this.columns = columns; this.isReversed = isReversed; this.readPlan = readPlan; - this.writePlan = ReplicaPlans.forReadRepair(partitionKey.getToken(), readPlan); + if (readPlan instanceof ReplicaPlan.ForTokenRead) + this.repairPlan = ((ReplicaPlan.ForTokenRead)readPlan).repairPlan(); + else + this.repairPlan = ((ReplicaPlan.ForRangeRead)readPlan).repairPlan(partitionKey.getToken()); int size = readPlan.contacts().size(); this.writeBackTo = new BitSet(size); @@ -96,18 +98,18 @@ public class RowIteratorMergeListener> int i = 0; for (Replica replica : readPlan.contacts()) { - if (writePlan.contacts().endpoints().contains(replica.endpoint())) + if (repairPlan.contacts().endpoints().contains(replica.endpoint())) writeBackTo.set(i); ++i; } } - // If we are contacting any nodes we didn't read from, we are likely handling a range movement. + // If we are contacting any nodes we didn't read from, we are handling a range movement (the likeliest scenario is a pending replica). // In this case we need to send all differences to these nodes, as we do not (with present design) know which // node they bootstrapped from, and so which data we need to duplicate. // In reality, there will be situations where we are simply sending the same number of writes to different nodes // and in this case we could probably avoid building a full difference, and only ensure each write makes it to // some other node, but it is probably not worth special casing this scenario. - this.buildFullDiff = Iterables.any(writePlan.contacts().endpoints(), e -> !readPlan.contacts().endpoints().contains(e)); + this.buildFullDiff = Iterables.any(repairPlan.contacts().endpoints(), e -> !readPlan.contacts().endpoints().contains(e)); this.repairs = new PartitionUpdate.Builder[size + (buildFullDiff ? 1 : 0)]; this.currentRows = new Row.Builder[size]; this.sourceDeletionTime = new DeletionTime[size]; @@ -376,12 +378,12 @@ public class RowIteratorMergeListener> if (buildFullDiff && repairs[repairs.length - 1] != null) fullDiffRepair = repairs[repairs.length - 1].build(); - Map mutations = Maps.newHashMapWithExpectedSize(writePlan.contacts().size()); + Map mutations = Maps.newHashMapWithExpectedSize(repairPlan.contacts().size()); ObjectIntHashMap sourceIds = new ObjectIntHashMap<>(((repairs.length + 1) * 4) / 3); for (int i = 0 ; i < readPlan.contacts().size() ; ++i) sourceIds.put(readPlan.contacts().get(i).endpoint(), 1 + i); - for (Replica replica : writePlan.contacts()) + for (Replica replica : repairPlan.contacts()) { PartitionUpdate update = null; int i = -1 + sourceIds.get(replica.endpoint()); @@ -397,6 +399,6 @@ public class RowIteratorMergeListener> mutations.put(replica, mutation); } - readRepair.repairPartition(partitionKey, mutations, writePlan); + readRepair.repairPartition(partitionKey, mutations, repairPlan); } } diff --git a/src/java/org/apache/cassandra/streaming/DataMovement.java b/src/java/org/apache/cassandra/streaming/DataMovement.java new file mode 100644 index 0000000000..f183281f97 --- /dev/null +++ b/src/java/org/apache/cassandra/streaming/DataMovement.java @@ -0,0 +1,111 @@ +/* + * 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.streaming; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ownership.MovementMap; + +public class DataMovement +{ + public static final IVersionedSerializer serializer = new Serializer(); + public final String operationId; + public final String streamOperation; + public final MovementMap movements; + + public DataMovement(String operationId, String streamOperation, MovementMap movements) + { + this.operationId = operationId; + this.streamOperation = streamOperation; + this.movements = movements; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(DataMovement t, DataOutputPlus out, int version) throws IOException + { + out.writeUTF(t.operationId); + out.writeUTF(t.streamOperation); + MovementMap.serializer.serialize(t.movements, out, version); + } + + @Override + public DataMovement deserialize(DataInputPlus in, int version) throws IOException + { + return new DataMovement(in.readUTF(), + in.readUTF(), + MovementMap.serializer.deserialize(in, version)); + } + + @Override + public long serializedSize(DataMovement t, int version) + { + return TypeSizes.sizeof(t.operationId) + + TypeSizes.sizeof(t.streamOperation) + + MovementMap.serializer.serializedSize(t.movements, version); + } + } + + public static class Status + { + public static final Serializer serializer = new Serializer(); + public final boolean success; + public final String operationType; + public final String operationId; + + public Status(boolean success, String operationType, String operationId) + { + this.success = success; + this.operationType = operationType; + this.operationId = operationId; + } + + static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(Status t, DataOutputPlus out, int version) throws IOException + { + out.writeBoolean(t.success); + out.writeUTF(t.operationType); + out.writeUTF(t.operationId); + } + + @Override + public Status deserialize(DataInputPlus in, int version) throws IOException + { + return new Status(in.readBoolean(), + in.readUTF(), + in.readUTF()); + } + + @Override + public long serializedSize(Status t, int version) + { + return TypeSizes.sizeof(t.success) + + TypeSizes.sizeof(t.operationType) + + TypeSizes.sizeof(t.operationId); + } + } + } +} diff --git a/src/java/org/apache/cassandra/streaming/DataMovementVerbHandler.java b/src/java/org/apache/cassandra/streaming/DataMovementVerbHandler.java new file mode 100644 index 0000000000..2b1d791ac7 --- /dev/null +++ b/src/java/org/apache/cassandra/streaming/DataMovementVerbHandler.java @@ -0,0 +1,112 @@ +/* + * 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.streaming; + +import java.io.IOException; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.Schema; + +public class DataMovementVerbHandler implements IVerbHandler +{ + private static final Logger logger = LoggerFactory.getLogger(DataMovementVerbHandler.class); + public static final DataMovementVerbHandler instance = new DataMovementVerbHandler(); + + @Override + public void doVerb(Message message) throws IOException + { + MessagingService.instance().respond(NoPayload.noPayload, message); // let coordinator know we received the message + StreamPlan streamPlan = new StreamPlan(StreamOperation.fromString(message.payload.streamOperation)); + Schema.instance.getNonLocalStrategyKeyspaces().stream().forEach((ksm) -> { + if (ksm.replicationStrategy.getReplicationFactor().allReplicas <= 1) + return; + + message.payload.movements.get(ksm.params.replication).asMap().forEach((local, endpoints) -> { + assert local.isSelf(); + boolean transientAdded = false; + boolean fullAdded = false; + for (Replica remote : DatabaseDescriptor.getEndpointSnitch().sortedByProximity(local.endpoint(), endpoints).filter(ep -> FailureDetector.instance.isAlive(ep.endpoint()))) + { + assert !remote.isSelf(); + if (remote.isFull() && !fullAdded) + { + streamPlan.requestRanges(remote.endpoint(), ksm.name, RangesAtEndpoint.of(local), RangesAtEndpoint.empty(local.endpoint())); + fullAdded = true; + } + else if (remote.isTransient() && !transientAdded) + { + streamPlan.requestRanges(remote.endpoint(), ksm.name, RangesAtEndpoint.empty(local.endpoint()), RangesAtEndpoint.of(local)); + transientAdded = true; + } + + if (fullAdded && transientAdded) + break; + } + if (!fullAdded) + { + if (local.isFull() || !transientAdded) + { + logger.error("Found no sources to stream from for {}", local); + send(false, message.from(), message.payload.streamOperation, message.payload.operationId); + } + } + }); + }); + + streamPlan.execute().addEventListener(new StreamEventHandler() + { + @Override + public void handleStreamEvent(StreamEvent event) {} + + @Override + public void onSuccess(@Nullable StreamState streamState) + { + send(true, message.from(), message.payload.streamOperation, message.payload.operationId); + } + + @Override + public void onFailure(Throwable throwable) + { + send(false, message.from(), message.payload.streamOperation, message.payload.operationId); + } + }); + + } + + private static void send(boolean success, InetAddressAndPort to, String operationType, String operationId) + { + MessagingService.instance().send(Message.out(Verb.DATA_MOVEMENT_EXECUTED_REQ, + new DataMovement.Status(success, operationType, operationId)), + to); + } +} diff --git a/src/java/org/apache/cassandra/streaming/StreamReceivedOutOfTokenRangeException.java b/src/java/org/apache/cassandra/streaming/StreamReceivedOutOfTokenRangeException.java new file mode 100644 index 0000000000..b7a2824e77 --- /dev/null +++ b/src/java/org/apache/cassandra/streaming/StreamReceivedOutOfTokenRangeException.java @@ -0,0 +1,49 @@ +/* + * 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.streaming; + +import java.util.Collection; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; + +public class StreamReceivedOutOfTokenRangeException extends RuntimeException +{ + private final Collection> ownedRanges; + private final DecoratedKey key; + private final String filename; + + public StreamReceivedOutOfTokenRangeException(Collection> ownedRanges, + DecoratedKey key, + String filename) + { + this.ownedRanges = ownedRanges; + this.key = key; + this.filename = filename; + } + + public String getMessage() + { + return String.format("Received stream for sstable %s containing key %s outside of owned ranges %s ", + filename, + key, + ownedRanges); + } +} diff --git a/src/java/org/apache/cassandra/streaming/StreamRequestOutOfTokenRangeException.java b/src/java/org/apache/cassandra/streaming/StreamRequestOutOfTokenRangeException.java new file mode 100644 index 0000000000..77dbee1524 --- /dev/null +++ b/src/java/org/apache/cassandra/streaming/StreamRequestOutOfTokenRangeException.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.streaming; + +import java.util.Collection; + +public class StreamRequestOutOfTokenRangeException extends RuntimeException +{ + private final Collection requests; + + public StreamRequestOutOfTokenRangeException(Collection requests) + { + this.requests = requests; + } + + public String getMessage() + { + return String.format("Received stream requests containing ranges outside of owned ranges: %s", requests); + } +} diff --git a/src/java/org/apache/cassandra/streaming/StreamSession.java b/src/java/org/apache/cassandra/streaming/StreamSession.java index d76f1f6d25..f89a2569ec 100644 --- a/src/java/org/apache/cassandra/streaming/StreamSession.java +++ b/src/java/org/apache/cassandra/streaming/StreamSession.java @@ -40,8 +40,10 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import io.netty.channel.Channel; @@ -57,6 +59,7 @@ import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.CompactionStrategyManager; +import org.apache.cassandra.dht.OwnedRanges; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.util.File; @@ -66,6 +69,7 @@ import org.apache.cassandra.locator.Replica; import org.apache.cassandra.metrics.StreamingMetrics; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.async.StreamingMultiplexedChannel; import org.apache.cassandra.streaming.messages.*; import org.apache.cassandra.utils.FBUtilities; @@ -111,7 +115,7 @@ import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; * * 3. Streaming phase * - * (a) The streaming phase is started at each node by calling {@link StreamSession#startStreamingFiles(boolean)}. + * (a) The streaming phase is started at each node by calling {@link StreamSession#startStreamingFiles(PrepareDirection)}. * This will send, sequentially on each outbound streaming connection (see {@link StreamingMultiplexedChannel}), * an {@link OutgoingStreamMessage} for each stream in each of the {@link StreamTransferTask}. * Each {@link OutgoingStreamMessage} consists of a {@link StreamMessageHeader} that contains metadata about @@ -399,7 +403,7 @@ public class StreamSession getPendingRepair(), getPreviewKind()); - channel.sendControlMessage(message).sync(); + sendControlMessage(message).sync(); onInitializationComplete(); } catch (Exception e) @@ -678,7 +682,7 @@ public class StreamSession prepare.summaries.add(task.getSummary()); } - channel.sendControlMessage(prepare).syncUninterruptibly(); + sendControlMessage(prepare).syncUninterruptibly(); } /** @@ -712,7 +716,7 @@ public class StreamSession if (channel.connected()) { state(State.FAILED); // make sure subsequent error handling sees the session in a final state - channel.sendControlMessage(new SessionFailedMessage()).awaitUninterruptibly(); + sendControlMessage(new SessionFailedMessage()).awaitUninterruptibly(); } StringBuilder failureReason = new StringBuilder("Failed because of an unknown exception\n"); boundStackTrace(e, DEBUG_STACKTRACE_LIMIT, failureReason); @@ -741,19 +745,23 @@ public class StreamSession /** * Prepare this session for sending/receiving files. + * + * @return the prepare future for testing */ - public void prepare(Collection requests, Collection summaries) + public Future prepare(Collection requests, Collection summaries) { // prepare tasks state(State.PREPARING); - ScheduledExecutors.nonPeriodicTasks.execute(() -> { + return ScheduledExecutors.nonPeriodicTasks.submit(() -> { try { prepareAsync(requests, summaries); + return null; } catch (Exception e) { onError(e); + return e; } }); } @@ -767,12 +775,12 @@ public class StreamSession * Finish preparing the session. This method is blocking (memtables are flushed in {@link #addTransferRanges}), * so the logic should not execute on the main IO thread (read: netty event loop). */ - private void prepareAsync(Collection requests, Collection summaries) + @VisibleForTesting + void prepareAsync(Collection requests, Collection summaries) { if (StreamOperation.REPAIR == streamOperation()) checkAvailableDiskSpaceAndCompactions(summaries); - for (StreamRequest request : requests) - addTransferRanges(request.keyspace, RangesAtEndpoint.concat(request.full, request.transientReplicas), request.columnFamilies, true); // always flush on stream request + processStreamRequests(requests); for (StreamSummary summary : summaries) prepareReceiving(summary); @@ -789,7 +797,7 @@ public class StreamSession // see CASSANDRA-17116 if (isPreview()) state(State.COMPLETE); - channel.sendControlMessage(prepareSynAck).syncUninterruptibly(); + sendControlMessage(prepareSynAck).syncUninterruptibly(); if (isPreview()) completePreview(); @@ -808,7 +816,7 @@ public class StreamSession // only send the (final) ACK if we are expecting the peer to send this node (the initiator) some files if (!isPreview()) - channel.sendControlMessage(new PrepareAckMessage()).syncUninterruptibly(); + sendControlMessage(new PrepareAckMessage()).syncUninterruptibly(); } if (isPreview()) @@ -824,6 +832,36 @@ public class StreamSession startStreamingFiles(PrepareDirection.ACK); } + protected Future sendControlMessage(StreamMessage message) + { + return channel.sendControlMessage(message); + } + + private void processStreamRequests(Collection requests) + { + List rejectedRequests = new ArrayList<>(); + + // group requests by keyspace + Multimap requestsByKeyspace = ArrayListMultimap.create(); + requests.forEach(r -> requestsByKeyspace.put(r.keyspace, r)); + + requestsByKeyspace.asMap().forEach((ks, reqs) -> + { + OwnedRanges ownedRanges = StorageService.instance.getNormalizedLocalRanges(ks, getBroadcastAddressAndPort()); + + reqs.forEach(req -> + { + RangesAtEndpoint allRangesAtEndpoint = RangesAtEndpoint.concat(req.full, req.transientReplicas); + if (ownedRanges.validateRangeRequest(allRangesAtEndpoint.ranges(), "Stream #" + planId(), "stream request", peer)) + addTransferRanges(req.keyspace, allRangesAtEndpoint, req.columnFamilies, true); // always flush on stream request + else + rejectedRequests.add(req); + }); + }); + + if (!rejectedRequests.isEmpty()) + throw new StreamRequestOutOfTokenRangeException(rejectedRequests); + } /** * In the case where we have an error checking disk space we allow the Operation to continue. * In the case where we do _not_ have available space, this method raises a RTE. @@ -1022,7 +1060,7 @@ public class StreamSession StreamingMetrics.totalIncomingBytes.inc(headerSize); metrics.incomingBytes.inc(headerSize); // send back file received message - channel.sendControlMessage(new ReceivedMessage(message.header.tableId, message.header.sequenceNumber)).syncUninterruptibly(); + sendControlMessage(new ReceivedMessage(message.header.tableId, message.header.sequenceNumber)).syncUninterruptibly(); StreamHook.instance.reportIncomingStream(message.header.tableId, message.stream, this, message.header.sequenceNumber); long receivedStartNanos = nanoTime(); try @@ -1105,7 +1143,7 @@ public class StreamSession // before sending the message (without closing the channel) // see CASSANDRA-17116 state(State.COMPLETE); - channel.sendControlMessage(new CompleteMessage()).syncUninterruptibly(); + sendControlMessage(new CompleteMessage()).syncUninterruptibly(); closeSession(State.COMPLETE); } @@ -1222,7 +1260,7 @@ public class StreamSession // pass the session planId/index to the OFM (which is only set at init(), after the transfers have already been created) ofm.header.addSessionInfo(this); // do not sync here as this does disk access - channel.sendControlMessage(ofm); + sendControlMessage(ofm); } } else @@ -1325,7 +1363,7 @@ public class StreamSession logger.info("[Stream #{}] Aborting stream session with peer {}...", planId(), peer); if (channel.connected()) - channel.sendControlMessage(new SessionFailedMessage()); + sendControlMessage(new SessionFailedMessage()); try { diff --git a/src/java/org/apache/cassandra/tcm/AbstractLocalProcessor.java b/src/java/org/apache/cassandra/tcm/AbstractLocalProcessor.java new file mode 100644 index 0000000000..2462bb2c5b --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/AbstractLocalProcessor.java @@ -0,0 +1,202 @@ +/* + * 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.tcm; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.exceptions.ExceptionCode.SERVER_ERROR; + +public abstract class AbstractLocalProcessor implements Processor +{ + private static final Logger logger = LoggerFactory.getLogger(PaxosBackedProcessor.class); + + protected final LocalLog log; + + public AbstractLocalProcessor(LocalLog log) + { + this.log = log; + } + + /** + * Epoch returned by processor in the Result is _not_ guaranteed to be visible by the Follower by + * the time when this method returns. + */ + @Override + public final Commit.Result commit(Entry.Id entryId, Transformation transform, final Epoch lastKnown, Retry.Deadline retryPolicy) + { + while (!retryPolicy.reachedMax()) + { + ClusterMetadata previous = log.waitForHighestConsecutive(); + if (!previous.fullCMSMembers().contains(FBUtilities.getBroadcastAddressAndPort())) + throw new IllegalStateException("Node is not a member of CMS anymore"); + Transformation.Result result; + if (!CassandraRelevantProperties.TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES.getBoolean() && + !transform.allowDuringUpgrades() && + previous.metadataSerializationUpgradeInProgress()) + { + result = new Transformation.Rejected(INVALID, "Upgrade in progress, can't commit " + transform); + } + else + { + result = executeStrictly(previous, transform); + } + + // If we got a rejection, it could be that _we_ are not aware of the highest epoch. + // Just try to catch up to the latest distributed state. + if (result.isRejected()) + { + ClusterMetadata replayed = fetchLogAndWait(null, retryPolicy); + + // Retry if replay has changed the epoch, return rejection otherwise. + if (!replayed.epoch.isAfter(previous.epoch)) + { + return maybeFailure(entryId, + lastKnown, + () -> new Commit.Result.Failure(result.rejected().code, result.rejected().reason, true)); + } + + continue; + } + + try + { + Epoch nextEpoch = result.success().metadata.epoch; + // If metadata applies, try committing it to the log + boolean applied = tryCommitOne(entryId, transform, + previous.epoch, nextEpoch, + previous.period, previous.nextPeriod(), + result.success().metadata.lastInPeriod); + + // Application here semantially means "succeeded in committing to the distributed log". + if (applied) + { + logger.info("Committed {}. New epoch is {}", transform, nextEpoch); + log.append(new Entry(entryId, nextEpoch, new Transformation.Executed(transform, result))); + log.awaitAtLeast(nextEpoch); + + return new Commit.Result.Success(result.success().metadata.epoch, + toReplication(result.success(), entryId, lastKnown, transform)); + } + else + { + retryPolicy.maybeSleep(); + // TODO: could also add epoch from mis-application from [applied]. + fetchLogAndWait(null, retryPolicy); + } + } + catch (Throwable e) + { + logger.error("Caught error while trying to perform a local commit", e); + JVMStabilityInspector.inspectThrowable(e); + retryPolicy.maybeSleep(); + } + } + return new Commit.Result.Failure(SERVER_ERROR, + String.format("Could not perform commit after %d/%d tries. Time remaining: %dms", + retryPolicy.tries, retryPolicy.maxTries, + TimeUnit.NANOSECONDS.toMillis(retryPolicy.remainingNanos())), + false); + } + + public Commit.Result maybeFailure(Entry.Id entryId, Epoch lastKnown, Supplier orElse) + { + Replication replication = toReplication(lastKnown); + Epoch commitedAt = null; + for (Entry entry : replication.entries()) + { + if (entry.id.equals(entryId)) + commitedAt = entry.epoch; + } + + // Succeeded after retry + if (commitedAt != null) + return new Commit.Result.Success(commitedAt, replication); + else + return orElse.get(); + } + + /** + * Calls {@link Transformation#execute(ClusterMetadata)}, but catches any + * {@link Transformation.RejectedTransformationException}, which is used by implementations to indicate that a known + * and ultimately fatal error has been encountered. Throwing such an error implies that the transformation has not + * succeeded and will not succeed if executed again, so no retries should be attempted. These scenarios are rare and + * using an unchecked exception for this purpose enables us to propagate fatal errors without polluting the + * {@link Transformation}interface . Here, those exceptions are caught and a {@link Transformation.Rejected} + * response returned, shortcircuiting any retry logic intended to mitigate more transient failures. + * + * @param metadata the starting state + * @param transformation to be applied to the starting state + * @return result of the application + */ + private Transformation.Result executeStrictly(ClusterMetadata metadata, Transformation transformation) + { + try + { + return transformation.execute(metadata); + } + catch (Transformation.RejectedTransformationException e) + { + return new Transformation.Rejected(INVALID, e.getMessage()); + } + } + + + private Replication toReplication(Transformation.Success success, Entry.Id entryId, Epoch lastKnown, Transformation transform) + { + if (lastKnown == null || lastKnown.isDirectlyBefore(success.metadata.epoch)) + return Replication.of(new Entry(entryId, success.metadata.epoch, transform)); + else + return toReplication(lastKnown); + } + + private Replication toReplication(Epoch lastKnown) + { + Replication replication; + if (lastKnown == null) + replication = Replication.EMPTY; + else + { + // We can use local log here since we always call this method only if local log is up-to-date: + // in case of a successful commit, we apply against latest metadata locally before committing, + // and in case of a rejection, we fetch latest entries to verify linearizability. + replication = log.getCommittedEntries(lastKnown); + } + + return replication; + } + + + @Override + public abstract ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy); + protected abstract boolean tryCommitOne(Entry.Id entryId, Transformation transform, + Epoch previousEpoch, Epoch nextEpoch, + long previousPeriod, long nextPeriod, boolean sealPeriod); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/AtomicLongBackedProcessor.java b/src/java/org/apache/cassandra/tcm/AtomicLongBackedProcessor.java new file mode 100644 index 0000000000..ba8729dc01 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/AtomicLongBackedProcessor.java @@ -0,0 +1,182 @@ +/* + * 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.tcm; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.SortedMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.log.Replication; + +/** + * This Processor should only be used for tests and for offline tools where we need to create tables without + * persisting any transformations. + */ +public class AtomicLongBackedProcessor extends AbstractLocalProcessor +{ + private static final Logger logger = LoggerFactory.getLogger(AtomicLongBackedProcessor.class); + + private final AtomicLong epochHolder; + + public AtomicLongBackedProcessor(LocalLog log) + { + this(log, false); + } + + public AtomicLongBackedProcessor(LocalLog log, boolean isReset) + { + super(log); + Epoch epoch = log.metadata().epoch; + assert epoch.is(Epoch.EMPTY) || isReset : epoch + " != " + Epoch.EMPTY; + this.epochHolder = new AtomicLong(epoch.getEpoch()); + } + + @Override + protected boolean tryCommitOne(Entry.Id entryId, Transformation transform, + Epoch previousEpoch, Epoch nextEpoch, + long previousPeriod, long nextPeriod, boolean sealPeriod) + { + if (epochHolder.get() == 0) + { + assert previousEpoch.is(Epoch.FIRST) : previousEpoch + " != " + Epoch.FIRST; + if (!epochHolder.compareAndSet(Epoch.EMPTY.getEpoch(), Epoch.FIRST.getEpoch())) + return false; + } + return epochHolder.compareAndSet(previousEpoch.getEpoch(), nextEpoch.getEpoch()); + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retry) + { + return log.waitForHighestConsecutive(); + } + + public static class InMemoryStorage implements LogStorage + { + private final List entries; + public final MetadataSnapshots metadataSnapshots; + + public InMemoryStorage() + { + this.entries = new ArrayList<>(); + this.metadataSnapshots = new InMemoryMetadataSnapshots(); + } + + @Override + public synchronized void append(long period, Entry entry) + { + boolean needsSorting = entries.isEmpty() ? false : entry.epoch.isDirectlyAfter(entries.get(entries.size() - 1).epoch); + entries.add(entry); + if (needsSorting) + Collections.sort(entries); + } + + @Override + public synchronized LogState getLogState(Epoch since) + { + ClusterMetadata latestSnapshot = metadataSnapshots.getLatestSnapshotAfter(since); + LogState logState; + if (latestSnapshot == null) + logState = new LogState(null, getReplication(since)); + else + logState = new LogState(latestSnapshot, getReplication(latestSnapshot.epoch)); + + return logState; + } + + @Override + public synchronized Replication getReplication(Epoch since) + { + assert entries != null : "Preserve history is off; can't query log state"; + int idx = indexedBinarySearch(entries, since, e -> e.epoch); + if (idx < 0) + idx = 0; + return Replication.of(entries.subList(idx, entries.size())); + } + + @Override + public synchronized Replication getReplication(long startPeriod, Epoch since) + { + return getReplication(since); + } + + private static > int indexedBinarySearch(List list, T2 key, Function fn) { + int low = 0; + int high = list.size()-1; + + while (low <= high) { + int mid = (low + high) >>> 1; + T1 midVal = list.get(mid); + int cmp = fn.apply(midVal).compareTo(key); + + if (cmp < 0) + low = mid + 1; + else if (cmp > 0) + high = mid - 1; + else + return mid; // key found + } + return -(low + 1); // key not found + } + } + + public static class InMemoryMetadataSnapshots implements MetadataSnapshots + { + private SortedMap snapshots; + + public InMemoryMetadataSnapshots() + { + this.snapshots = new ConcurrentSkipListMap<>(); + } + + @Override + public ClusterMetadata getLatestSnapshotAfter(Epoch epoch) + { + if (snapshots.isEmpty()) + return null; + Epoch latest = snapshots.lastKey(); + if (latest.isAfter(epoch)) + return snapshots.get(latest); + return null; + } + + @Override + public ClusterMetadata getSnapshot(Epoch epoch) + { + return snapshots.get(epoch); + } + + @Override + public void storeSnapshot(ClusterMetadata metadata) + { + this.snapshots.put(metadata.epoch, metadata); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/CMSOperations.java b/src/java/org/apache/cassandra/tcm/CMSOperations.java new file mode 100644 index 0000000000..6aaccf7f83 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/CMSOperations.java @@ -0,0 +1,199 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.cms.AdvanceCMSReconfiguration; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.MBeanWrapper; + +public class CMSOperations implements CMSOperationsMBean +{ + public static final String MBEAN_OBJECT_NAME = "org.apache.cassandra.tcm:type=CMSOperations"; + + private static final Logger logger = LoggerFactory.getLogger(ClusterMetadataService.class); + public static CMSOperations instance = new CMSOperations(ClusterMetadataService.instance()); + + public static void initJmx() + { + MBeanWrapper.instance.registerMBean(instance, MBEAN_OBJECT_NAME); + } + + private final ClusterMetadataService cms; + + private CMSOperations(ClusterMetadataService cms) + { + this.cms = cms; + } + + @Override + public void initializeCMS(List ignoredEndpoints) + { + cms.upgradeFromGossip(ignoredEndpoints); + } + + @Override + public void resumeReconfigureCms() + { + InProgressSequences.finishInProgressSequences(ReconfigureCMS.SequenceKey.instance); + } + + + @Override + public void reconfigureCMS(int rf, boolean sync) + { + Runnable r = () -> cms.reconfigureCMS(ReplicationParams.simpleMeta(rf, ClusterMetadata.current().directory.knownDatacenters())); + if (sync) + r.run(); + else + ScheduledExecutors.nonPeriodicTasks.submit(r); + } + + @Override + public void reconfigureCMS(Map rf, boolean sync) + { + Runnable r = () -> ClusterMetadataService.instance().reconfigureCMS(ReplicationParams.ntsMeta(rf)); + if (sync) + r.run(); + else + ScheduledExecutors.nonPeriodicTasks.submit(r); + } + + @Override + public Map> reconfigureCMSStatus() + { + ClusterMetadata metadata = ClusterMetadata.current(); + ReconfigureCMS sequence = (ReconfigureCMS) metadata.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + if (sequence == null) + return null; + + AdvanceCMSReconfiguration advance = sequence.next; + Map> status = new LinkedHashMap<>(); // to preserve order + if (advance.activeTransition != null) + status.put("ACTIVE", Collections.singletonList(metadata.directory.endpoint(advance.activeTransition.nodeId).toString())); + + if (!advance.diff.additions.isEmpty()) + status.put("ADDITIONS", advance.diff.additions.stream() + .map(metadata.directory::endpoint) + .map(Object::toString) + .collect(Collectors.toList())); + + if (!advance.diff.removals.isEmpty()) + status.put("REMOVALS", advance.diff.removals.stream() + .map(metadata.directory::endpoint) + .map(Object::toString) + .collect(Collectors.toList())); + + return status; + } + + @Override + public Map describeCMS() + { + Map info = new HashMap<>(); + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService service = ClusterMetadataService.instance(); + String members = metadata.fullCMSMembers().stream().sorted().map(Object::toString).collect(Collectors.joining(",")); + info.put("MEMBERS", members); + info.put("IS_MEMBER", Boolean.toString(service.isCurrentMember(FBUtilities.getBroadcastAddressAndPort()))); + info.put("SERVICE_STATE", ClusterMetadataService.state(metadata).toString()); + info.put("IS_MIGRATING", Boolean.toString(service.isMigrating())); + info.put("EPOCH", Long.toString(metadata.epoch.getEpoch())); + info.put("LOCAL_PENDING", Integer.toString(ClusterMetadataService.instance().log().pendingBufferSize())); + info.put("COMMITS_PAUSED", Boolean.toString(service.commitsPaused())); + info.put("REPLICATION_FACTOR", ReplicationParams.meta(metadata).toString()); + return info; + } + + @Override + public void sealPeriod() + { + logger.info("Sealing current period in metadata log"); + long period = ClusterMetadataService.instance().sealPeriod().period; + logger.info("Current period {} is sealed", period); + } + + @Override + public void unsafeRevertClusterMetadata(long epoch) + { + if (!DatabaseDescriptor.getUnsafeTCMMode()) + throw new IllegalStateException("Cluster is not running unsafe TCM mode, can't revert epoch"); + ClusterMetadataService.instance().revertToEpoch(Epoch.create(epoch)); + } + + @Override + public String dumpClusterMetadata(long epoch, long transformToEpoch, String version) throws IOException + { + return ClusterMetadataService.instance().dumpClusterMetadata(Epoch.create(epoch), Epoch.create(transformToEpoch), Version.valueOf(version)); + } + + @Override + public String dumpClusterMetadata() throws IOException + { + return dumpClusterMetadata(Epoch.EMPTY.getEpoch(), + ClusterMetadata.current().epoch.getEpoch() + 1000, + NodeVersion.CURRENT.serializationVersion().toString()); + } + + @Override + public void unsafeLoadClusterMetadata(String file) throws IOException + { + if (!DatabaseDescriptor.getUnsafeTCMMode()) + throw new IllegalStateException("Cluster is not running unsafe TCM mode, can't load cluster metadata " + file); + ClusterMetadataService.instance().loadClusterMetadata(file); + } + + @Override + public void setCommitsPaused(boolean paused) + { + if (paused) + ClusterMetadataService.instance().pauseCommits(); + else + ClusterMetadataService.instance().resumeCommits(); + } + + @Override + public boolean getCommitsPaused() + { + return ClusterMetadataService.instance().commitsPaused(); + } + + @Override + public boolean cancelInProgressSequences(String sequenceOwner, String expectedSequenceKind) + { + return InProgressSequences.cancelInProgressSequences(sequenceOwner, expectedSequenceKind); + } +} diff --git a/src/java/org/apache/cassandra/tcm/CMSOperationsMBean.java b/src/java/org/apache/cassandra/tcm/CMSOperationsMBean.java new file mode 100644 index 0000000000..76854a6674 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/CMSOperationsMBean.java @@ -0,0 +1,45 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface CMSOperationsMBean +{ + public void initializeCMS(List ignore); + public void resumeReconfigureCms(); + public void reconfigureCMS(int rf, boolean sync); + public void reconfigureCMS(Map rf, boolean sync); + public Map> reconfigureCMSStatus(); + + public Map describeCMS(); + public void sealPeriod(); + + public void unsafeRevertClusterMetadata(long epoch); + public String dumpClusterMetadata(long epoch, long transformToEpoch, String version) throws IOException; + public String dumpClusterMetadata() throws IOException; + public void unsafeLoadClusterMetadata(String file) throws IOException; + + public void setCommitsPaused(boolean paused); + public boolean getCommitsPaused(); + + public boolean cancelInProgressSequences(String sequenceOwner, String expectedSequenceKind); +} diff --git a/src/java/org/apache/cassandra/tcm/ClusterMetadata.java b/src/java/org/apache/cassandra/tcm/ClusterMetadata.java new file mode 100644 index 0000000000..3c7144eee4 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ClusterMetadata.java @@ -0,0 +1,945 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.extensions.ExtensionKey; +import org.apache.cassandra.tcm.extensions.ExtensionValue; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PrimaryRangeComparator; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.ownership.TokenMap; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR; +import static org.apache.cassandra.db.TypeSizes.sizeof; + +public class ClusterMetadata +{ + public static final Serializer serializer = new Serializer(); + + public final Epoch epoch; + public final long period; + public final boolean lastInPeriod; + public final IPartitioner partitioner; // Set during (initial) construction and not modifiable via Transformer + + public final DistributedSchema schema; + public final Directory directory; + public final TokenMap tokenMap; + public final DataPlacements placements; + public final LockedRanges lockedRanges; + public final InProgressSequences inProgressSequences; + public final ImmutableMap, ExtensionValue> extensions; + + // These two fields are lazy but only for the test purposes, since their computation requires initialization of the log ks + private Set fullCMSReplicas; + private Set fullCMSEndpoints; + + public ClusterMetadata(IPartitioner partitioner) + { + this(partitioner, Directory.EMPTY); + } + + @VisibleForTesting + public ClusterMetadata(IPartitioner partitioner, Directory directory) + { + this(partitioner, directory, DistributedSchema.first()); + } + + @VisibleForTesting + public ClusterMetadata(IPartitioner partitioner, Directory directory, DistributedSchema schema) + { + this(Epoch.EMPTY, + Period.EMPTY, + true, + partitioner, + schema, + directory, + new TokenMap(partitioner), + DataPlacements.EMPTY, + LockedRanges.EMPTY, + InProgressSequences.EMPTY, + ImmutableMap.of()); + } + + public ClusterMetadata(Epoch epoch, + long period, + boolean lastInPeriod, + IPartitioner partitioner, + DistributedSchema schema, + Directory directory, + TokenMap tokenMap, + DataPlacements placements, + LockedRanges lockedRanges, + InProgressSequences inProgressSequences, + Map, ExtensionValue> extensions) + { + // TODO: token map is a feature of the specific placement strategy, and so may not be a relevant component of + // ClusterMetadata in the long term. We need to consider how the actual components of metadata can be evolved + // over time. + assert tokenMap == null || tokenMap.partitioner().getClass().equals(partitioner.getClass()) : "Partitioner for TokenMap doesn't match base partitioner"; + this.epoch = epoch; + this.period = period; + this.lastInPeriod = lastInPeriod; + this.partitioner = partitioner; + this.schema = schema; + this.directory = directory; + this.tokenMap = tokenMap; + this.placements = placements; + this.lockedRanges = lockedRanges; + this.inProgressSequences = inProgressSequences; + this.extensions = ImmutableMap.copyOf(extensions); + } + + public Set fullCMSMembers() + { + if (fullCMSEndpoints == null) + this.fullCMSEndpoints = ImmutableSet.copyOf(placements.get(ReplicationParams.meta(this)).reads.byEndpoint().keySet()); + return fullCMSEndpoints; + } + + public Set fullCMSMembersAsReplicas() + { + if (fullCMSReplicas == null) + this.fullCMSReplicas = ImmutableSet.copyOf(placements.get(ReplicationParams.meta(this)).reads.byEndpoint().flattenValues()); + return fullCMSReplicas; + } + + public boolean isCMSMember(InetAddressAndPort endpoint) + { + return fullCMSMembers().contains(endpoint); + } + + public Transformer transformer() + { + return new Transformer(this, this.nextEpoch(), false); + } + + public Transformer transformer(boolean sealPeriod) + { + return new Transformer(this, this.nextEpoch(), sealPeriod); + } + + public ClusterMetadata forceEpoch(Epoch epoch) + { + // In certain circumstances, the last modified epoch of the individual + // components may have been updated beyond the epoch we're specifying here. + // An example is the execution of an UnsafeJoin transformation, where the + // sub-steps (Start/Mid/Finish) are executed in series, each updating a + // single ClusterMetadata and its individual components. At the end of that + // sequence, the CM epoch is then set forcibly to ensure the UnsafeJoin only + // increments the published epoch by one. As each component has its own last + // modified epoch, we may also need to coerce those, but only if they are + // greater than the epoch we're forcing here. + return new ClusterMetadata(epoch, + period, + lastInPeriod, + partitioner, + capLastModified(schema, epoch), + capLastModified(directory, epoch), + capLastModified(tokenMap, epoch), + capLastModified(placements, epoch), + capLastModified(lockedRanges, epoch), + capLastModified(inProgressSequences, epoch), + capLastModified(extensions, epoch)); + } + + public ClusterMetadata forcePeriod(long period) + { + return new ClusterMetadata(epoch, + period, + false, + partitioner, + schema, + directory, + tokenMap, + placements, + lockedRanges, + inProgressSequences, + extensions); + } + + private static Map, ExtensionValue> capLastModified(Map, ExtensionValue> original, Epoch maxEpoch) + { + Map, ExtensionValue> updated = new HashMap<>(); + original.forEach((key, value) -> { + ExtensionValue newValue = value == null || value.lastModified().isEqualOrBefore(maxEpoch) + ? value + : (ExtensionValue)value.withLastModified(maxEpoch); + updated.put(key, newValue); + }); + return updated; + } + + @SuppressWarnings("unchecked") + private static V capLastModified(MetadataValue value, Epoch maxEpoch) + { + return value == null || value.lastModified().isEqualOrBefore(maxEpoch) + ? (V)value + : value.withLastModified(maxEpoch); + } + + public Epoch nextEpoch() + { + return epoch.nextEpoch(); + } + + public long nextPeriod() + { + return lastInPeriod ? period + 1 : period; + } + + public DataPlacement writePlacementAllSettled(KeyspaceMetadata ksm) + { + List joining = new ArrayList<>(); + List leaving = new ArrayList<>(); + List moving = new ArrayList<>(); + + for (Map.Entry entry : directory.states.entrySet()) + { + switch (entry.getValue()) + { + case BOOTSTRAPPING: + joining.add(entry.getKey()); + break; + case LEAVING: + leaving.add(entry.getKey()); + break; + case MOVING: + moving.add(entry.getKey()); + break; + } + } + + Transformer t = transformer(); + for (NodeId node: joining) + { + MultiStepOperation joinSequence = inProgressSequences.get(node); + assert joinSequence instanceof BootstrapAndJoin; + Set tokens = ((BootstrapAndJoin)joinSequence).finishJoin.tokens; + t = t.proposeToken(node, tokens); + } + for (NodeId node : leaving) + t = t.proposeRemoveNode(node); + // todo: add tests for move! + for (NodeId node : moving) + t = t.proposeRemoveNode(node).proposeToken(node, tokenMap.tokens(node)); + + ClusterMetadata proposed = t.build().metadata; + return ClusterMetadataService.instance() + .placementProvider() + .calculatePlacements(epoch, proposed.tokenMap.toRanges(), proposed, Keyspaces.of(ksm)) + .get(ksm.params.replication); + } + + // TODO Remove this as it isn't really an equivalent to the previous concept of pending ranges + public boolean hasPendingRangesFor(KeyspaceMetadata ksm, Token token) + { + PlacementForRange writes = placements.get(ksm.params.replication).writes; + PlacementForRange reads = placements.get(ksm.params.replication).reads; + return !reads.forToken(token).equals(writes.forToken(token)); + } + + // TODO Remove this as it isn't really an equivalent to the previous concept of pending ranges + public boolean hasPendingRangesFor(KeyspaceMetadata ksm, InetAddressAndPort endpoint) + { + PlacementForRange writes = placements.get(ksm.params.replication).writes; + PlacementForRange reads = placements.get(ksm.params.replication).reads; + return !writes.byEndpoint().get(endpoint).equals(reads.byEndpoint().get(endpoint)); + } + + public Collection> localWriteRanges(KeyspaceMetadata metadata) + { + return placements.get(metadata.params.replication).writes.byEndpoint().get(FBUtilities.getBroadcastAddressAndPort()).ranges(); + } + + // TODO Remove this as it isn't really an equivalent to the previous concept of pending ranges + public Map, VersionedEndpoints.ForRange> pendingRanges(KeyspaceMetadata metadata) + { + Map, VersionedEndpoints.ForRange> map = new HashMap<>(); + PlacementForRange writes = placements.get(metadata.params.replication).writes; + PlacementForRange reads = placements.get(metadata.params.replication).reads; + + // first, pending ranges as the result of range splitting or merging + // i.e. new ranges being created through join/leave + List> pending = new ArrayList<>(writes.ranges()); + pending.removeAll(reads.ranges()); + for (Range p : pending) + map.put(p, placements.get(metadata.params.replication).writes.forRange(p)); + + // next, ranges where the ranges themselves are not changing, but the replicas are + // i.e. replacement or RF increase + writes.replicaGroups().forEach((range, endpoints) -> { + VersionedEndpoints.ForRange readGroup = reads.forRange(range); + if (!readGroup.equals(endpoints)) + map.put(range, VersionedEndpoints.forRange(endpoints.lastModified(), + endpoints.get().filter(r -> !readGroup.get().contains(r)))); + }); + + return map; + } + + // TODO Remove this as it isn't really an equivalent to the previous concept of pending endpoints + public VersionedEndpoints.ForToken pendingEndpointsFor(KeyspaceMetadata metadata, Token t) + { + VersionedEndpoints.ForToken writeEndpoints = placements.get(metadata.params.replication).writes.forToken(t); + VersionedEndpoints.ForToken readEndpoints = placements.get(metadata.params.replication).reads.forToken(t); + EndpointsForToken.Builder endpointsForToken = writeEndpoints.get().newBuilder(writeEndpoints.size() - readEndpoints.size()); + + for (Replica writeReplica : writeEndpoints.get()) + { + if (!readEndpoints.get().contains(writeReplica)) + endpointsForToken.add(writeReplica); + } + return VersionedEndpoints.forToken(writeEndpoints.lastModified(), endpointsForToken.build()); + } + + public static class Transformer + { + private final ClusterMetadata base; + private final Epoch epoch; + private final long period; + private final boolean lastInPeriod; + private final IPartitioner partitioner; + private DistributedSchema schema; + private Directory directory; + private TokenMap tokenMap; + private DataPlacements placements; + private LockedRanges lockedRanges; + private InProgressSequences inProgressSequences; + private final Map, ExtensionValue> extensions; + private final Set modifiedKeys; + + private Transformer(ClusterMetadata metadata, Epoch epoch, boolean lastInPeriod) + { + this.base = metadata; + this.epoch = epoch; + this.period = metadata.nextPeriod(); + this.lastInPeriod = lastInPeriod; + this.partitioner = metadata.partitioner; + this.schema = metadata.schema; + this.directory = metadata.directory; + this.tokenMap = metadata.tokenMap; + this.placements = metadata.placements; + this.lockedRanges = metadata.lockedRanges; + this.inProgressSequences = metadata.inProgressSequences; + extensions = new HashMap<>(metadata.extensions); + modifiedKeys = new HashSet<>(); + } + + public Transformer with(DistributedSchema schema) + { + this.schema = schema; + return this; + } + + public Transformer with(Directory directory) + { + this.directory = directory; + return this; + } + + public Transformer register(NodeAddresses addresses, Location location, NodeVersion version) + { + directory = directory.with(addresses, location, version); + return this; + } + + public Transformer unregister(NodeId nodeId) + { + directory = directory.without(nodeId); + return this; + } + + public Transformer withNewAddresses(NodeId nodeId, NodeAddresses addresses) + { + directory = directory.withNodeAddresses(nodeId, addresses); + return this; + } + + public Transformer withVersion(NodeId nodeId, NodeVersion version) + { + directory = directory.withNodeVersion(nodeId, version); + return this; + } + + public Transformer withNodeState(NodeId id, NodeState state) + { + directory = directory.withNodeState(id, state); + return this; + } + + public Transformer proposeToken(NodeId nodeId, Collection tokens) + { + tokenMap = tokenMap.assignTokens(nodeId, tokens); + return this; + } + + public Transformer addToRackAndDC(NodeId nodeId) + { + directory = directory.withRackAndDC(nodeId); + return this; + } + + public Transformer unproposeTokens(NodeId nodeId) + { + tokenMap = tokenMap.unassignTokens(nodeId); + directory = directory.withoutRackAndDC(nodeId); + return this; + } + + public Transformer moveTokens(NodeId nodeId, Collection tokens) + { + tokenMap = tokenMap.unassignTokens(nodeId) + .assignTokens(nodeId, tokens); + return this; + } + + public Transformer join(NodeId nodeId) + { + directory = directory.withNodeState(nodeId, NodeState.JOINED); + return this; + } + + public Transformer replaced(NodeId replaced, NodeId replacement) + { + Collection transferringTokens = tokenMap.tokens(replaced); + tokenMap = tokenMap.unassignTokens(replaced) + .assignTokens(replacement, transferringTokens); + directory = directory.without(replaced) + .withRackAndDC(replacement) + .withNodeState(replacement, NodeState.JOINED); + return this; + } + + public Transformer proposeRemoveNode(NodeId id) + { + tokenMap = tokenMap.unassignTokens(id); + return this; + } + + public Transformer left(NodeId id) + { + tokenMap = tokenMap.unassignTokens(id); + directory = directory.withNodeState(id, NodeState.LEFT) + .withoutRackAndDC(id); + return this; + } + + public Transformer with(DataPlacements placements) + { + this.placements = placements; + return this; + } + + public Transformer with(LockedRanges lockedRanges) + { + this.lockedRanges = lockedRanges; + return this; + } + + public Transformer with(InProgressSequences sequences) + { + this.inProgressSequences = sequences; + return this; + } + + public Transformer with(ExtensionKey key, ExtensionValue obj) + { + if (MetadataKeys.CORE_METADATA.contains(key)) + throw new IllegalArgumentException("Core cluster metadata objects should be addressed directly, " + + "not using the associated MetadataKey"); + + if (!key.valueType.isInstance(obj)) + throw new IllegalArgumentException("Value of type " + obj.getClass() + + " is incompatible with type for key " + key + + " (" + key.valueType + ")"); + + extensions.put(key, obj); + modifiedKeys.add(key); + return this; + } + + public Transformer withIfAbsent(ExtensionKey key, ExtensionValue obj) + { + if (extensions.containsKey(key)) + return this; + return with(key, obj); + } + + public Transformer without(ExtensionKey key) + { + if (MetadataKeys.CORE_METADATA.contains(key)) + throw new IllegalArgumentException("Core cluster metadata objects should be addressed directly, " + + "not using the associated MetadataKey"); + if (extensions.remove(key) != null) + modifiedKeys.add(key); + return this; + } + + public Transformed build() + { + // Process extension first as a) these are actually mutable and b) they are added to the set of + // modified keys when added/updated/removed + for (MetadataKey key : modifiedKeys) + { + ExtensionValue mutable = extensions.get(key); + if (null != mutable) + mutable.withLastModified(epoch); + } + + if (schema != base.schema) + { + modifiedKeys.add(MetadataKeys.SCHEMA); + schema = schema.withLastModified(epoch); + } + + if (directory != base.directory) + { + modifiedKeys.add(MetadataKeys.NODE_DIRECTORY); + directory = directory.withLastModified(epoch); + } + + if (tokenMap != base.tokenMap) + { + modifiedKeys.add(MetadataKeys.TOKEN_MAP); + tokenMap = tokenMap.withLastModified(epoch); + } + + if (placements != base.placements) + { + modifiedKeys.add(MetadataKeys.DATA_PLACEMENTS); + // sort all endpoint lists to preserve primary replica + if (CassandraRelevantProperties.TCM_SORT_REPLICA_GROUPS.getBoolean()) + { + PrimaryRangeComparator comparator = new PrimaryRangeComparator(tokenMap, directory); + placements = DataPlacements.sortReplicaGroups(placements, comparator); + } + placements = placements.withLastModified(epoch); + } + + if (lockedRanges != base.lockedRanges) + { + modifiedKeys.add(MetadataKeys.LOCKED_RANGES); + lockedRanges = lockedRanges.withLastModified(epoch); + } + + if (inProgressSequences != base.inProgressSequences) + { + modifiedKeys.add(MetadataKeys.IN_PROGRESS_SEQUENCES); + inProgressSequences = inProgressSequences.withLastModified(epoch); + } + + return new Transformed(new ClusterMetadata(epoch, + period, + lastInPeriod, + partitioner, + schema, + directory, + tokenMap, + placements, + lockedRanges, + inProgressSequences, + extensions), + ImmutableSet.copyOf(modifiedKeys)); + } + + public ClusterMetadata buildForGossipMode() + { + return new ClusterMetadata(Epoch.UPGRADE_GOSSIP, + Period.EMPTY, + true, + partitioner, + schema, + directory, + tokenMap, + placements, + lockedRanges, + inProgressSequences, + extensions); + } + + @Override + public String toString() + { + return "Transformer{" + + "baseEpoch=" + base.epoch + + ", epoch=" + epoch + + ", lastInPeriod=" + lastInPeriod + + ", partitioner=" + partitioner + + ", schema=" + schema + + ", directory=" + schema + + ", tokenMap=" + tokenMap + + ", placement=" + placements + + ", lockedRanges=" + lockedRanges + + ", inProgressSequences=" + inProgressSequences + + ", extensions=" + extensions + + ", modifiedKeys=" + modifiedKeys + + '}'; + } + + public static class Transformed + { + public final ClusterMetadata metadata; + public final ImmutableSet modifiedKeys; + + public Transformed(ClusterMetadata metadata, ImmutableSet modifiedKeys) + { + this.metadata = metadata; + this.modifiedKeys = modifiedKeys; + } + } + } + + public String legacyToString() + { + StringBuilder sb = new StringBuilder(); + Set> normal = new HashSet<>(); + Set> bootstrapping = new HashSet<>(); + Set leaving = new HashSet<>(); + + for (Map.Entry entry : directory.states.entrySet()) + { + InetAddressAndPort endpoint = directory.endpoint(entry.getKey()); + switch (entry.getValue()) + { + case BOOTSTRAPPING: + for (Token t : tokenMap.tokens(entry.getKey())) + bootstrapping.add(Pair.create(t, endpoint)); + break; + case LEAVING: + leaving.add(endpoint); + break; + case JOINED: + for (Token t : tokenMap.tokens(entry.getKey())) + normal.add(Pair.create(t, endpoint)); + break; + case MOVING: + // todo when adding MOVE + break; + } + } + + if (!normal.isEmpty()) + { + sb.append("Normal Tokens:"); + sb.append(LINE_SEPARATOR.getString()); + for (Pair ep : normal) + { + sb.append(ep.right); + sb.append(':'); + sb.append(ep.left); + sb.append(LINE_SEPARATOR.getString()); + } + } + + if (!bootstrapping.isEmpty()) + { + sb.append("Bootstrapping Tokens:" ); + sb.append(LINE_SEPARATOR.getString()); + for (Pair entry : bootstrapping) + { + sb.append(entry.right).append(':').append(entry.left); + sb.append(LINE_SEPARATOR.getString()); + } + } + + if (!leaving.isEmpty()) + { + sb.append("Leaving Endpoints:"); + sb.append(LINE_SEPARATOR.getString()); + for (InetAddressAndPort ep : leaving) + { + sb.append(ep); + sb.append(LINE_SEPARATOR.getString()); + } + } + return sb.toString(); + } + + @Override + public String toString() + { + return "ClusterMetadata{" + + "epoch=" + epoch + + ", schema=" + schema + + ", directory=" + directory + + ", tokenMap=" + tokenMap + + ", placements=" + placements + + ", lockedRanges=" + lockedRanges + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof ClusterMetadata)) return false; + ClusterMetadata that = (ClusterMetadata) o; + return epoch.equals(that.epoch) && + lastInPeriod == that.lastInPeriod && + schema.equals(that.schema) && + directory.equals(that.directory) && + tokenMap.equals(that.tokenMap) && + placements.equals(that.placements) && + lockedRanges.equals(that.lockedRanges) && + inProgressSequences.equals(that.inProgressSequences) && + extensions.equals(that.extensions); + } + + private static final Logger logger = LoggerFactory.getLogger(ClusterMetadata.class); + + public void dumpDiff(ClusterMetadata other) + { + if (!epoch.equals(other.epoch)) + { + logger.warn("Epoch {} != {}", epoch, other.epoch); + } + if (lastInPeriod != other.lastInPeriod) + { + logger.warn("lastInPeriod {} != {}", lastInPeriod, other.lastInPeriod); + } + if (!schema.equals(other.schema)) + { + Keyspaces.KeyspacesDiff diff = Keyspaces.diff(schema.getKeyspaces(), other.schema.getKeyspaces()); + logger.warn("Schemas differ {}", diff); + } + if (!directory.equals(other.directory)) + { + logger.warn("Directories differ:"); + directory.dumpDiff(other.directory); + } + if (!tokenMap.equals(other.tokenMap)) + { + logger.warn("Token maps differ:"); + tokenMap.dumpDiff(other.tokenMap); + } + if (!placements.equals(other.placements)) + { + logger.warn("Placements differ:"); + placements.dumpDiff(other.placements); + } + if (!lockedRanges.equals(other.lockedRanges)) + { + logger.warn("Locked ranges differ: {} != {}", lockedRanges, other.lockedRanges); + } + if (!inProgressSequences.equals(other.inProgressSequences)) + { + logger.warn("In progress sequences differ: {} != {}", inProgressSequences, other.inProgressSequences); + } + if (!extensions.equals(other.extensions)) + { + logger.warn("Extensions differ: {} != {}", extensions, other.extensions); + } + } + + @Override + public int hashCode() + { + return Objects.hash(epoch, lastInPeriod, schema, directory, tokenMap, placements, lockedRanges, inProgressSequences, extensions); + } + + public static ClusterMetadata current() + { + return ClusterMetadataService.instance().metadata(); + } + + /** + * Startup of some services may race with cluster metadata initialization. We allow those services to + * gracefully handle scenarios when it is not yet initialized. + */ + public static ClusterMetadata currentNullable() + { + ClusterMetadataService service = ClusterMetadataService.instance(); + if (service == null) + return null; + return service.metadata(); + } + + public NodeId myNodeId() + { + return directory.peerId(FBUtilities.getBroadcastAddressAndPort()); + } + + public NodeState myNodeState() + { + NodeId nodeId = myNodeId(); + if (myNodeId() != null) + return directory.peerState(nodeId); + return null; + } + + public boolean metadataSerializationUpgradeInProgress() + { + return !directory.clusterMaxVersion.serializationVersion().equals(directory.clusterMinVersion.serializationVersion()); + } + + public static class Serializer implements MetadataSerializer + { + @Override + public void serialize(ClusterMetadata metadata, DataOutputPlus out, Version version) throws IOException + { + if (version.isAtLeast(Version.V1)) + out.writeUTF(metadata.partitioner.getClass().getCanonicalName()); + + Epoch.serializer.serialize(metadata.epoch, out); + out.writeUnsignedVInt(metadata.period); + out.writeBoolean(metadata.lastInPeriod); + + if (version.isBefore(Version.V1)) + out.writeUTF(metadata.partitioner.getClass().getCanonicalName()); + + DistributedSchema.serializer.serialize(metadata.schema, out, version); + Directory.serializer.serialize(metadata.directory, out, version); + TokenMap.serializer.serialize(metadata.tokenMap, out, version); + DataPlacements.serializer.serialize(metadata.placements, out, version); + LockedRanges.serializer.serialize(metadata.lockedRanges, out, version); + InProgressSequences.serializer.serialize(metadata.inProgressSequences, out, version); + out.writeInt(metadata.extensions.size()); + for (Map.Entry, ExtensionValue> entry : metadata.extensions.entrySet()) + { + ExtensionKey key = entry.getKey(); + ExtensionValue value = entry.getValue(); + ExtensionKey.serializer.serialize(key, out, version); + assert key.valueType.isInstance(value); + value.serialize(out, version); + } + } + + @Override + public ClusterMetadata deserialize(DataInputPlus in, Version version) throws IOException + { + IPartitioner partitioner = null; + if (version.isAtLeast(Version.V1)) + partitioner = FBUtilities.newPartitioner(in.readUTF()); + + Epoch epoch = Epoch.serializer.deserialize(in); + long period = in.readUnsignedVInt(); + boolean lastInPeriod = in.readBoolean(); + + if (version.isBefore(Version.V1)) + partitioner = FBUtilities.newPartitioner(in.readUTF()); + + DistributedSchema schema = DistributedSchema.serializer.deserialize(in, version); + Directory dir = Directory.serializer.deserialize(in, version); + TokenMap tokenMap = TokenMap.serializer.deserialize(in, version); + DataPlacements placements = DataPlacements.serializer.deserialize(in, version); + LockedRanges lockedRanges = LockedRanges.serializer.deserialize(in, version); + InProgressSequences ips = InProgressSequences.serializer.deserialize(in, version); + int items = in.readInt(); + Map, ExtensionValue> extensions = new HashMap<>(items); + for (int i = 0; i < items; i++) + { + ExtensionKey key = ExtensionKey.serializer.deserialize(in, version); + ExtensionValue value = key.newValue(); + value.deserialize(in, version); + extensions.put(key, value); + } + return new ClusterMetadata(epoch, + period, + lastInPeriod, + partitioner, + schema, + dir, + tokenMap, + placements, + lockedRanges, + ips, + extensions); + } + + @Override + public long serializedSize(ClusterMetadata metadata, Version version) + { + long size = TypeSizes.INT_SIZE; + for (Map.Entry, ExtensionValue> entry : metadata.extensions.entrySet()) + size += ExtensionKey.serializer.serializedSize(entry.getKey(), version) + + entry.getValue().serializedSize(version); + + size += Epoch.serializer.serializedSize(metadata.epoch) + + VIntCoding.computeUnsignedVIntSize(metadata.period) + + TypeSizes.BOOL_SIZE + + sizeof(metadata.partitioner.getClass().getCanonicalName()) + + DistributedSchema.serializer.serializedSize(metadata.schema, version) + + Directory.serializer.serializedSize(metadata.directory, version) + + TokenMap.serializer.serializedSize(metadata.tokenMap, version) + + DataPlacements.serializer.serializedSize(metadata.placements, version) + + LockedRanges.serializer.serializedSize(metadata.lockedRanges, version) + + InProgressSequences.serializer.serializedSize(metadata.inProgressSequences, version); + + return size; + } + + public static IPartitioner getPartitioner(DataInputPlus in, Version version) throws IOException + { + if (version.isAtLeast(Version.V1)) + return FBUtilities.newPartitioner(in.readUTF()); + + Epoch.serializer.deserialize(in); + in.readUnsignedVInt(); + in.readBoolean(); + return FBUtilities.newPartitioner(in.readUTF()); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/ClusterMetadataService.java b/src/java/org/apache/cassandra/tcm/ClusterMetadataService.java new file mode 100644 index 0000000000..01abe4abdf --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ClusterMetadataService.java @@ -0,0 +1,860 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.exceptions.ExceptionCode; +import org.apache.cassandra.io.util.FileInputStreamPlus; +import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.migration.Election; +import org.apache.cassandra.tcm.migration.GossipProcessor; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; +import org.apache.cassandra.tcm.transformations.SealPeriod; +import org.apache.cassandra.tcm.transformations.cms.PrepareCMSReconfiguration; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.stream.Collectors.toSet; +import static org.apache.cassandra.tcm.ClusterMetadataService.State.GOSSIP; +import static org.apache.cassandra.tcm.ClusterMetadataService.State.LOCAL; +import static org.apache.cassandra.tcm.ClusterMetadataService.State.REMOTE; +import static org.apache.cassandra.tcm.ClusterMetadataService.State.RESET; +import static org.apache.cassandra.tcm.compatibility.GossipHelper.emptyWithSchemaFromSystemTables; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.utils.Collectors3.toImmutableSet; + +public class ClusterMetadataService +{ + private static final Logger logger = LoggerFactory.getLogger(ClusterMetadataService.class); + + private static ClusterMetadataService instance; + private static Throwable trace; + + public static void setInstance(ClusterMetadataService newInstance) + { + if (instance != null) + throw new IllegalStateException(String.format("Cluster metadata is already initialized to %s.", instance), + trace); + instance = newInstance; + trace = new RuntimeException("Previously initialized trace"); + } + + @VisibleForTesting + public static ClusterMetadataService unsetInstance() + { + ClusterMetadataService tmp = instance(); + instance = null; + return tmp; + } + + public static ClusterMetadataService instance() + { + return instance; + } + + private final PlacementProvider placementProvider; + private final Processor processor; + private final LocalLog log; + private final MetadataSnapshots snapshots; + + private final IVerbHandler replicationHandler; + private final IVerbHandler logNotifyHandler; + private final IVerbHandler fetchLogHandler; + private final IVerbHandler commitRequestHandler; + private final CurrentEpochRequestHandler currentEpochHandler; + + private final PeerLogFetcher peerLogFetcher; + + private final AtomicBoolean commitsPaused = new AtomicBoolean(); + + public static State state() + { + return state(ClusterMetadata.current()); + } + + public static State state(ClusterMetadata metadata) + { + if (CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA.isPresent()) + return RESET; + + if (metadata.epoch.isBefore(Epoch.EMPTY)) + return GOSSIP; + + // The node is a full member of the CMS if it has started participating in reads for distributed metadata table (which + // implies it is a write replica as well). In other words, it's a fully joined member of the replica set responsible for + // the distributed metadata table. + if (ClusterMetadata.current().isCMSMember(FBUtilities.getBroadcastAddressAndPort())) + return LOCAL; + return REMOTE; + } + + ClusterMetadataService(PlacementProvider placementProvider, + Function wrapProcessor, + Supplier cmsStateSupplier, + LocalLog.LogSpec logSpec) + { + this.placementProvider = placementProvider; + this.snapshots = new MetadataSnapshots.SystemKeyspaceMetadataSnapshots(); + + Processor localProcessor; + LogStorage logStorage = LogStorage.SystemKeyspace; + if (CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.getBoolean()) + { + log = LocalLog.sync(logSpec); + localProcessor = wrapProcessor.apply(new AtomicLongBackedProcessor(log, logSpec.isReset())); + fetchLogHandler = new FetchCMSLog.Handler((e, ignored) -> logStorage.getLogState(e)); + } + else + { + log = LocalLog.async(logSpec); + localProcessor = wrapProcessor.apply(new PaxosBackedProcessor(log)); + fetchLogHandler = new FetchCMSLog.Handler(); + } + + Commit.Replicator replicator = CassandraRelevantProperties.TCM_USE_NO_OP_REPLICATOR.getBoolean() + ? Commit.Replicator.NO_OP + : new Commit.DefaultReplicator(() -> log.metadata().directory); + + RemoteProcessor remoteProcessor = new RemoteProcessor(log, Discovery.instance::discoveredNodes); + GossipProcessor gossipProcessor = new GossipProcessor(); + currentEpochHandler = new CurrentEpochRequestHandler(); + + commitRequestHandler = new Commit.Handler(localProcessor, replicator, cmsStateSupplier); + processor = new SwitchableProcessor(localProcessor, + remoteProcessor, + gossipProcessor, + replicator, + cmsStateSupplier); + + + replicationHandler = new Replication.ReplicationHandler(log); + logNotifyHandler = new Replication.LogNotifyHandler(log); + peerLogFetcher = new PeerLogFetcher(log); + } + + @VisibleForTesting + // todo: convert this to a factory method with an obvious name that this is just for testing + public ClusterMetadataService(PlacementProvider placementProvider, + MetadataSnapshots snapshots, + LocalLog log, + Processor processor, + Commit.Replicator replicator, + boolean isMemberOfOwnershipGroup) + { + this.placementProvider = placementProvider; + this.log = log; + this.processor = new SwitchableProcessor(processor, null, null, replicator, () -> State.LOCAL); + this.snapshots = snapshots; + + replicationHandler = new Replication.ReplicationHandler(log); + logNotifyHandler = new Replication.LogNotifyHandler(log); + currentEpochHandler = new CurrentEpochRequestHandler(); + + fetchLogHandler = isMemberOfOwnershipGroup ? new FetchCMSLog.Handler() : null; + commitRequestHandler = isMemberOfOwnershipGroup ? new Commit.Handler(processor, replicator, () -> LOCAL) : null; + + peerLogFetcher = new PeerLogFetcher(log); + } + + private ClusterMetadataService(PlacementProvider placementProvider, + MetadataSnapshots snapshots, + LocalLog log, + Processor processor, + Replication.ReplicationHandler replicationHandler, + Replication.LogNotifyHandler logNotifyHandler, + CurrentEpochRequestHandler currentEpochHandler, + FetchCMSLog.Handler fetchLogHandler, + Commit.Handler commitRequestHandler, + PeerLogFetcher peerLogFetcher) + { + this.placementProvider = placementProvider; + this.snapshots = snapshots; + this.log = log; + this.processor = processor; + this.replicationHandler = replicationHandler; + this.logNotifyHandler = logNotifyHandler; + this.currentEpochHandler = currentEpochHandler; + this.fetchLogHandler = fetchLogHandler; + this.commitRequestHandler = commitRequestHandler; + this.peerLogFetcher = peerLogFetcher; + } + + @SuppressWarnings("resource") + public static void initializeForTools(boolean loadSSTables) + { + if (instance != null) + return; + ClusterMetadata emptyFromSystemTables = emptyWithSchemaFromSystemTables(Collections.singleton("DC1")); + emptyFromSystemTables.schema.initializeKeyspaceInstances(DistributedSchema.empty(), loadSSTables); + emptyFromSystemTables = emptyFromSystemTables.forceEpoch(Epoch.EMPTY); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(emptyFromSystemTables) + .withStorage(new AtomicLongBackedProcessor.InMemoryStorage()); + LocalLog log = LocalLog.sync(logSpec); + log.ready(); + ClusterMetadataService cms = new ClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + new Replication.ReplicationHandler(log), + new Replication.LogNotifyHandler(log), + new CurrentEpochRequestHandler(), + null, + null, + new PeerLogFetcher(log)); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + ClusterMetadataService.setInstance(cms); + } + + @SuppressWarnings("resource") + public static void initializeForClients() + { + if (instance != null) + return; + + ClusterMetadataService.setInstance(StubClusterMetadataService.forClientTools()); + } + + public static void initializeForClients(DistributedSchema initialSchema) + { + if (instance != null) + return; + + ClusterMetadataService.setInstance(StubClusterMetadataService.forClientTools(initialSchema)); + } + + public boolean isCurrentMember(InetAddressAndPort peer) + { + return ClusterMetadata.current().isCMSMember(peer); + } + + public void upgradeFromGossip(List ignoredEndpoints) + { + Set ignored = ignoredEndpoints.stream().map(InetAddressAndPort::getByNameUnchecked).collect(toSet()); + if (ignored.contains(FBUtilities.getBroadcastAddressAndPort())) + { + String msg = String.format("Can't ignore local host %s when doing CMS migration", FBUtilities.getBroadcastAddressAndPort()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + ClusterMetadata metadata = metadata(); + Set existingMembers = metadata.fullCMSMembers(); + + if (!metadata.directory.allAddresses().containsAll(ignored)) + { + Set allAddresses = Sets.newHashSet(metadata.directory.allAddresses()); + String msg = String.format("Ignored host(s) %s don't exist in the cluster", Sets.difference(ignored, allAddresses)); + logger.error(msg); + throw new IllegalStateException(msg); + } + + for (Map.Entry entry : metadata.directory.versions.entrySet()) + { + NodeVersion version = entry.getValue(); + InetAddressAndPort ep = metadata.directory.getNodeAddresses(entry.getKey()).broadcastAddress; + if (ignored.contains(ep)) + { + // todo; what do we do if an endpoint has a mismatching gossip-clustermetadata? + // - we could add the node to --ignore and force this CM to it? + // - require operator to bounce/manually fix the CM on that node + // for now just requiring that any ignored host is also down +// if (FailureDetector.instance.isAlive(ep)) +// throw new IllegalStateException("Can't ignore " + ep + " during CMS migration - it is not down"); + logger.info("Endpoint {} running {} is ignored", ep, version); + continue; + } + + if (!version.isUpgraded()) + { + String msg = String.format("All nodes are not yet upgraded - %s is running %s", metadata.directory.endpoint(entry.getKey()), version); + logger.error(msg); + throw new IllegalStateException(msg); + } + } + + if (existingMembers.isEmpty()) + { + logger.info("First CMS node"); + Set candidates = metadata + .directory + .allAddresses() + .stream() + .filter(ep -> !FBUtilities.getBroadcastAddressAndPort().equals(ep) && + !ignored.contains(ep)) + .collect(toImmutableSet()); + + Election.instance.nominateSelf(candidates, ignored, metadata::equals, metadata); + ClusterMetadataService.instance().sealPeriod(); + } + else + { + throw new IllegalStateException("Can't upgrade from gossip since CMS is already initialized"); + } + } + + // This method is to be used _only_ for interactive purposes (i.e. nodetool), since it assumes no retries are going to be attempted on reject. + public void reconfigureCMS(ReplicationParams replicationParams) + { + Transformation transformation = new PrepareCMSReconfiguration.Complex(replicationParams); + + ClusterMetadataService.instance() + .commit(transformation); + + InProgressSequences.finishInProgressSequences(ReconfigureCMS.SequenceKey.instance); + } + + public boolean applyFromGossip(ClusterMetadata expected, ClusterMetadata updated) + { + logger.debug("Applying from gossip, current={} new={}", expected, updated); + if (!expected.epoch.isBefore(Epoch.EMPTY)) + throw new IllegalStateException("Can't apply a ClusterMetadata from gossip with epoch " + expected.epoch); + if (state() != GOSSIP) + throw new IllegalStateException("Can't apply a ClusterMetadata from gossip when CMSState is not GOSSIP: " + state()); + + return log.unsafeSetCommittedFromGossip(expected, updated); + } + + public void setFromGossip(ClusterMetadata fromGossip) + { + logger.debug("Setting from gossip, new={}", fromGossip); + if (state() != GOSSIP) + throw new IllegalStateException("Can't apply a ClusterMetadata from gossip when CMSState is not GOSSIP: " + state()); + log.unsafeSetCommittedFromGossip(fromGossip); + } + + public void forceSnapshot(ClusterMetadata snapshot) + { + commit(new ForceSnapshot(snapshot)); + } + + public void revertToEpoch(Epoch epoch) + { + logger.warn("Reverting to epoch {}", epoch); + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadata toApply = transformSnapshot(LogState.getForRecovery(epoch)) + .forceEpoch(metadata.epoch.nextEpoch()) + .forcePeriod(metadata.nextPeriod()); + forceSnapshot(toApply); + } + + /** + * dumps the cluster metadata at the given epoch, returns path to the generated file + * + * if the given Epoch is EMPTY, we dump the current metadata + * + * @param epoch dump clustermetadata at this epoch + * @param transformToEpoch transform the dumped metadata to this epoch + * @param version serialisation version + */ + public String dumpClusterMetadata(Epoch epoch, Epoch transformToEpoch, Version version) throws IOException + { + ClusterMetadata toDump = epoch.isAfter(Epoch.EMPTY) + ? transformSnapshot(LogState.getForRecovery(epoch)) + : ClusterMetadata.current(); + toDump = toDump.forceEpoch(transformToEpoch); + Path p = Files.createTempFile("clustermetadata", "dump"); + try (FileOutputStreamPlus out = new FileOutputStreamPlus(p)) + { + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, toDump, out, version); + } + logger.info("Dumped cluster metadata to {}", p.toString()); + return p.toString(); + } + + public void loadClusterMetadata(String file) throws IOException + { + logger.warn("Loading cluster metadata from {}", file); + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadata toApply = deserializeClusterMetadata(file) + .forceEpoch(metadata.epoch.nextEpoch()) + .forcePeriod(metadata.nextPeriod()); + forceSnapshot(toApply); + } + + public static ClusterMetadata deserializeClusterMetadata(String file) throws IOException + { + try (FileInputStreamPlus fisp = new FileInputStreamPlus(file)) + { + return VerboseMetadataSerializer.deserialize(ClusterMetadata.serializer, fisp); + } + } + + private ClusterMetadata transformSnapshot(LogState state) + { + ClusterMetadata toApply = state.baseState; + for (Entry entry : state.transformations.entries()) + { + Transformation.Result res = entry.transform.execute(toApply); + assert res.isSuccess(); + toApply = res.success().metadata; + } + return toApply; + } + + public final Supplier entryIdGen = new Entry.DefaultEntryIdGen(); + + public interface CommitSuccessHandler + { + T accept(ClusterMetadata latest); + } + + public interface CommitFailureHandler + { + T accept(ExceptionCode code, String message); + } + + public ClusterMetadata commit(Transformation transform) + { + return commit(transform, + metadata -> metadata, + (code, message) -> { + throw new IllegalStateException(String.format("Can not commit transformation: \"%s\"(%s).", + code, message)); + }); + } + + /** + * Attempt to commit the transformation (with retries). + * + * Since we can not rely on reliability of the network or even the fact that the committing node will stay alive + * for the duration of commit, we have to allow for subsequent discovery of the transformation effects, which can + * be made visible either by replaying the log, or receiving the metadata snapshot. + * + * In other words, there is no reliable way to find out whether _this particular_ transformation has been executed + * while we are allowing replay from snapshot, since even failure response from the CMS does not guarantee + * Paxos re-proposal, which would place the transformation into the log during proposal _by some other_ CMS node. + * + * Protocol does foresee the concept of EntryId that would allow discovery of the committed transformations + * without changes to binary protocol, but this change was left out from the initial implementation of TCM. + * + * @param onFailure handler checks if rejection has resulted from a retry of the same trasformation. + */ + public T1 commit(Transformation transform, CommitSuccessHandler onSuccess, CommitFailureHandler onFailure) + { + if (commitsPaused.get()) + throw new IllegalStateException("Commits are paused, not trying to commit " + transform); + + long startTime = nanoTime(); + // Replay everything in-flight before attempting a commit + // We grab highest consecutive epoch here, since we want both local and remote processors to benefit from + // discover-own-commits via entry id in case of lost messages (in remote case) and Paxos re-proposals (in local case) + Epoch highestConsecutive = log.waitForHighestConsecutive().epoch; + + Commit.Result result = processor.commit(entryIdGen.get(), transform, highestConsecutive); + + try + { + if (result.isSuccess()) + { + TCMMetrics.instance.commitSuccessLatency.update(nanoTime() - startTime, NANOSECONDS); + return onSuccess.accept(awaitAtLeast(result.success().epoch)); + } + else + { + TCMMetrics.instance.recordCommitFailureLatency(nanoTime() - startTime, NANOSECONDS, result.failure().rejected); + return onFailure.accept(result.failure().code, result.failure().message); + } + } + catch (TimeoutException t) + { + throw new IllegalStateException(String.format("Timed out while waiting for the follower to enact the epoch %s", result.success().epoch), t); + } + catch (InterruptedException e) + { + throw new IllegalStateException("Couldn't commit the transformation. Is the node shutting down?", e); + } + } + + /** + * Accessors + */ + + public static IVerbHandler replicationHandler() + { + // Make it possible to get Verb without throwing NPE during simulation + ClusterMetadataService instance = ClusterMetadataService.instance(); + if (instance == null) + return null; + return instance.replicationHandler; + } + + public static IVerbHandler logNotifyHandler() + { + // Make it possible to get Verb without throwing NPE during simulation + ClusterMetadataService instance = ClusterMetadataService.instance(); + if (instance == null) + return null; + return instance.logNotifyHandler; + } + + public static IVerbHandler fetchLogRequestHandler() + { + // Make it possible to get Verb without throwing NPE during simulation + ClusterMetadataService instance = ClusterMetadataService.instance(); + if (instance == null) + return null; + return instance.fetchLogHandler; + } + + public static IVerbHandler commitRequestHandler() + { + // Make it possible to get Verb without throwing NPE during simulation + ClusterMetadataService instance = ClusterMetadataService.instance(); + if (instance == null) + return null; + return instance.commitRequestHandler; + } + + public static CurrentEpochRequestHandler currentEpochRequestHandler() + { + // Make it possible to get Verb without throwing NPE during simulation + ClusterMetadataService instance = ClusterMetadataService.instance(); + if (instance == null) + return null; + return instance.currentEpochHandler; + } + + public PlacementProvider placementProvider() + { + return this.placementProvider; + } + + @VisibleForTesting + public Processor processor() + { + return processor; + } + + @VisibleForTesting + public LocalLog log() + { + return log; + } + + public ClusterMetadata metadata() + { + return log.metadata(); + } + + /** + * Fetches log entries from directly from CMS, at least to the specified epoch. + * + * This operation is blocking and also waits for all retrieved log entries to be + * enacted, so on return all transformations to ClusterMetadata will be visible. + * @return metadata with all currently committed entries enacted. + */ + public ClusterMetadata fetchLogFromCMS(Epoch awaitAtLeast) + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (awaitAtLeast.isBefore(Epoch.FIRST)) + return metadata; + + Epoch ourEpoch = metadata.epoch; + + if (ourEpoch.isEqualOrAfter(awaitAtLeast)) + return metadata; + + Retry.Deadline deadline = Retry.Deadline.after(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.NANOSECONDS), + new Retry.Jitter(TCMMetrics.instance.fetchLogRetries)); + // responses for ALL withhout knowing we have pending + metadata = processor.fetchLogAndWait(awaitAtLeast, deadline); + if (metadata.epoch.isBefore(awaitAtLeast)) + { + throw new IllegalStateException(String.format("Could not catch up to epoch %s even after fetching log from CMS. Highest seen after fetching is %s.", + awaitAtLeast, ourEpoch)); + } + return metadata; + } + + /** + * Attempts to asynchronously retrieve log entries from a non-CMS peer. + * Fetches and applies the log state representing the delta between the current local epoch and the one requested. + * This is used when a message from a peer contains an epoch higher than the current local epoch. As the sender of + * the message must have seen and enacted the given epoch, they must (under normal circumstances) be able to supply + * any entries needed to catch up this node. + * When the returned future completes, the metadata it provides is the current published metadata at the + * moment of completion. In the expected case, this will have had any fetched transformations up to the requested + * epoch applied. If the fetch was unsuccessful (e.g. because the peer was unavailable) it will still be whatever + * the currently published metadata, but which entries have been enacted cannot be guaranteed. + * @param from peer to request log entries from + * @param awaitAtLeast the upper epoch required. It's expected that the peer is able to supply log entries up to at + * least this epoch. + * @return A future which will supply the current ClusterMetadata at the time of completion + */ + public Future fetchLogFromPeerAsync(InetAddressAndPort from, Epoch awaitAtLeast) + { + ClusterMetadata current = ClusterMetadata.current(); + if (FBUtilities.getBroadcastAddressAndPort().equals(from) || + current.epoch.isEqualOrAfter(awaitAtLeast) || + awaitAtLeast.isBefore(Epoch.FIRST)) + return ImmediateFuture.success(current); + + return peerLogFetcher.asyncFetchLog(from, awaitAtLeast); + } + + /** + * + * IMPORTANT: this call can return _without_ catching us up, so should only be used privately. + * + * Attempts to synchronously retrieve log entries from a non-CMS peer. + * Fetches the log state representing the delta between the current local epoch and the one supplied. + * This is to be used when a message from a peer contains an epoch higher than the current local epoch. As + * sender of the message must have seen and enacted the given epoch, they must (under normal circumstances) + * be able to supply any entries needed to catch up this node. + * The metadata returned is the current published metadata at that time. In the expected case, this will have had + * any fetched transformations up to the requested epoch applied. If the fetch was unsuccessful (e.g. because the + * peer was unavailable) it will still be whatever the currently published metadata, but which entries have been + * enacted cannot be guaranteed. + * @param from peer to request log entries from + * @param awaitAtLeast the upper epoch required. It's expected that the peer is able to supply log entries up to at + * least this epoch. + * @return The current ClusterMetadata at the time of completion + */ + private ClusterMetadata fetchLogFromPeer(ClusterMetadata metadata, InetAddressAndPort from, Epoch awaitAtLeast) + { + if (awaitAtLeast.isBefore(Epoch.FIRST) || FBUtilities.getBroadcastAddressAndPort().equals(from)) + return ClusterMetadata.current(); + Epoch before = metadata.epoch; + if (before.isEqualOrAfter(awaitAtLeast)) + return metadata; + return peerLogFetcher.fetchLogEntriesAndWait(from, awaitAtLeast); + } + + public Future fetchLogFromPeerOrCMSAsync(ClusterMetadata metadata, InetAddressAndPort from, Epoch awaitAtLeast) + { + AsyncPromise future = new AsyncPromise<>(); + ScheduledExecutors.optionalTasks.submit(() -> { + try + { + future.setSuccess(ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, from, awaitAtLeast)); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.warn(String.format("Learned about epoch %s from %s, but could not fetch log.", awaitAtLeast, from), t); + future.setFailure(t); + } + }); + return future; + } + + /** + * Combines {@link #fetchLogFromPeer} with {@link #fetchLogFromCMS} to synchronously fetch and apply log entries + * up to the requested epoch. The supplied peer will be contacted first and if after doing so, the current local + * metadata is not caught up to at least the required epoch, a further request is made to the CMS. + * The returned ClusterMetadata is guaranteed to have been published, though it may have also been superceded by + * further updates. + * If the requested epoch is not reached even after fetching from the CMS, an IllegalStateException is thrown. + * @param metadata a starting point for the fetch. If the requested epoch is <= the epoch of this metadata, the + * call is a no-op. It's expected that this is usually the current cluster metadata at the time of + * calling. + * @param from Initial peer to contact. Usually this is the sender of a message containing the requested epoch, + * which means it can be assumed that this peer (if available) can supply any missing log entries. + * @param awaitAtLeast The requested epoch. + * @return A published ClusterMetadata with all entries up to (at least) the requested epoch enacted. + * @throws IllegalStateException if the requested epoch could not be reached, even after falling back to CMS catchup + */ + public ClusterMetadata fetchLogFromPeerOrCMS(ClusterMetadata metadata, InetAddressAndPort from, Epoch awaitAtLeast) + { + if (awaitAtLeast.isBefore(Epoch.FIRST) || FBUtilities.getBroadcastAddressAndPort().equals(from)) + return metadata; + + Epoch before = metadata.epoch; + if (before.isEqualOrAfter(awaitAtLeast)) + return metadata; + + metadata = fetchLogFromPeer(metadata, from, awaitAtLeast); + if (metadata.epoch.isEqualOrAfter(awaitAtLeast)) + return metadata; + + metadata = fetchLogFromCMS(awaitAtLeast); + if (metadata.epoch.isBefore(awaitAtLeast)) + throw new IllegalStateException("Still behind after fetching log from CMS"); + logger.debug("Fetched log from CMS - caught up from epoch {} to epoch {}", before, metadata.epoch); + return metadata; + } + + public ClusterMetadata awaitAtLeast(Epoch epoch) throws InterruptedException, TimeoutException + { + return log.awaitAtLeast(epoch); + } + + public MetadataSnapshots snapshotManager() + { + return snapshots; + } + + public ClusterMetadata sealPeriod() + { + return ClusterMetadataService.instance.commit(SealPeriod.instance, + (ClusterMetadata metadata) -> metadata, + (code, reason) -> { + // If the transformation got rejected, someone else has beat us to seal this period + return ClusterMetadata.current(); + }); + } + + public void initRecentlySealedPeriodsIndex() + { + Sealed.initIndexFromSystemTables(); + } + + public boolean isMigrating() + { + return Election.instance.isMigrating(); + } + + public void migrated() + { + Election.instance.migrated(); + } + public void pauseCommits() + { + commitsPaused.set(true); + } + + public void resumeCommits() + { + commitsPaused.set(false); + } + + public boolean commitsPaused() + { + return commitsPaused.get(); + } + /** + * Switchable implementation that allow us to go between local and remote implementation whenever we need it. + * When the node becomes a member of CMS, it switches back to being a regular member of a cluster, and all + * the CMS handlers get disabled. + */ + @VisibleForTesting + public static class SwitchableProcessor implements Processor + { + private final Processor local; + private final RemoteProcessor remote; + private final GossipProcessor gossip; + private final Supplier cmsStateSupplier; + private final Commit.Replicator replicator; + + SwitchableProcessor(Processor local, + RemoteProcessor remote, + GossipProcessor gossip, + Commit.Replicator replicator, + Supplier cmsStateSupplier) + { + this.local = local; + this.remote = remote; + this.gossip = gossip; + this.replicator = replicator; + this.cmsStateSupplier = cmsStateSupplier; + } + + @VisibleForTesting + public Processor delegate() + { + return delegateInternal().right; + } + + private Pair delegateInternal() + { + State state = cmsStateSupplier.get(); + switch (state) + { + case LOCAL: + case RESET: + return Pair.create(state, local); + case REMOTE: + return Pair.create(state, remote); + case GOSSIP: + return Pair.create(state, gossip); + } + throw new IllegalStateException("Bad CMS state: " + state); + } + + @Override + public Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy) + { + Pair delegate = delegateInternal(); + Commit.Result result = delegate.right.commit(entryId, transform, lastKnown, retryPolicy); + if (delegate.left == LOCAL || delegate.left == RESET) + replicator.send(result, null); + return result; + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + return delegate().fetchLogAndWait(waitFor, retryPolicy); + } + + public String toString() + { + return "SwitchableProcessor{" + + delegate() + '}'; + } + } + + public enum State + { + LOCAL, REMOTE, GOSSIP, RESET + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/Commit.java b/src/java/org/apache/cassandra/tcm/Commit.java new file mode 100644 index 0000000000..8376e55ca4 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Commit.java @@ -0,0 +1,407 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.ExceptionCode; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.net.*; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static org.apache.cassandra.tcm.ClusterMetadataService.State.*; + +public class Commit +{ + private static final Logger logger = LoggerFactory.getLogger(Commit.class); + + public static final IVersionedSerializer defaultMessageSerializer = new Serializer(NodeVersion.CURRENT.serializationVersion()); + + private static volatile Serializer serializerCache; + public static IVersionedSerializer messageSerializer(Version version) + { + Serializer cached = serializerCache; + if (cached != null && cached.serializationVersion.equals(version)) + return cached; + cached = new Serializer(version); + serializerCache = cached; + return cached; + } + + private final Entry.Id entryId; + private final Transformation transform; + private final Epoch lastKnown; + + public Commit(Entry.Id entryId, Transformation transform, Epoch lastKnown) + { + this.entryId = entryId; + this.transform = transform; + this.lastKnown = lastKnown; + } + + public String toString() + { + return "Commit{" + + "transformation=" + transform + + ", lastKnown=" + lastKnown + + '}'; + } + + static class Serializer implements IVersionedSerializer + { + private final Version serializationVersion; + + public Serializer(Version serializationVersion) + { + this.serializationVersion = serializationVersion; + } + + public void serialize(Commit t, DataOutputPlus out, int version) throws IOException + { + out.writeInt(serializationVersion.asInt()); + Entry.Id.serializer.serialize(t.entryId, out, serializationVersion); + Transformation.transformationSerializer.serialize(t.transform, out, serializationVersion); + Epoch.serializer.serialize(t.lastKnown, out, serializationVersion); + } + + public Commit deserialize(DataInputPlus in, int version) throws IOException + { + Version deserializationVersion = Version.fromInt(in.readInt()); + Entry.Id entryId = Entry.Id.serializer.deserialize(in, deserializationVersion); + Transformation transform = Transformation.transformationSerializer.deserialize(in, deserializationVersion); + Epoch lastKnown = Epoch.serializer.deserialize(in, deserializationVersion); + return new Commit(entryId, transform, lastKnown); + } + + public long serializedSize(Commit t, int version) + { + return TypeSizes.sizeof(serializationVersion.asInt()) + + Transformation.transformationSerializer.serializedSize(t.transform, serializationVersion) + + Entry.Id.serializer.serializedSize(t.entryId, serializationVersion) + + Epoch.serializer.serializedSize(t.lastKnown, serializationVersion); + } + } + + static volatile Result.Serializer resultSerializerCache; + public static interface Result + { + boolean isSuccess(); + boolean isFailure(); + + default Success success() + { + return (Success) this; + } + + default Failure failure() + { + return (Failure) this; + } + IVersionedSerializer defaultMessageSerializer = new Serializer(NodeVersion.CURRENT.serializationVersion()); + + static IVersionedSerializer messageSerializer(Version version) + { + Serializer cached = resultSerializerCache; + if (cached != null && cached.serializationVersion.equals(version)) + return cached; + cached = new Serializer(version); + resultSerializerCache = cached; + return cached; + } + + final class Success implements Result + { + public final Epoch epoch; + public final Replication replication; + + public Success(Epoch epoch, Replication replication) + { + this.epoch = epoch; + this.replication = replication; + } + + @Override + public String toString() + { + return "Success{" + + "epoch=" + epoch + + ", replication=" + replication + + '}'; + } + + public boolean isSuccess() + { + return true; + } + + public boolean isFailure() + { + return false; + } + } + + final class Failure implements Result + { + public final ExceptionCode code; + public final String message; + // Rejection means that we were able to linearize the operation, + // but it was rejected by the internal logic of the transformation. + public final boolean rejected; + + public Failure(ExceptionCode code, String message, boolean rejected) + { + if (message == null) + message = ""; + this.code = code; + this.message = message; + this.rejected = rejected; + } + + @Override + public String toString() + { + return "Failure{" + + "code='" + code + '\'' + + "message='" + message + '\'' + + "rejected=" + rejected + + '}'; + } + + public boolean isSuccess() + { + return false; + } + + public boolean isFailure() + { + return true; + } + } + + class Serializer implements IVersionedSerializer + { + private static final byte SUCCESS = 1; + private static final byte REJECTED = 2; + private static final byte FAILED = 3; + + private final Version serializationVersion; + + public Serializer(Version serializationVersion) + { + this.serializationVersion = serializationVersion; + } + + @Override + public void serialize(Result t, DataOutputPlus out, int version) throws IOException + { + if (t instanceof Success) + { + out.writeByte(SUCCESS); + out.writeUnsignedVInt32(serializationVersion.asInt()); + Replication.serializer.serialize(t.success().replication, out, serializationVersion); + Epoch.serializer.serialize(t.success().epoch, out, serializationVersion); + } + else + { + assert t instanceof Failure; + Failure failure = (Failure) t; + out.writeByte(failure.rejected ? REJECTED : FAILED); + out.writeUnsignedVInt32(failure.code.value); + out.writeUTF(failure.message); + } + } + + @Override + public Result deserialize(DataInputPlus in, int version) throws IOException + { + int b = in.readByte(); + if (b == SUCCESS) + { + Version deserializationVersion = Version.fromInt(in.readUnsignedVInt32()); + Replication delta = Replication.serializer.deserialize(in, deserializationVersion); + Epoch epoch = Epoch.serializer.deserialize(in, deserializationVersion); + return new Success(epoch, delta); + } + else + { + return new Failure(ExceptionCode.fromValue(in.readUnsignedVInt32()), + in.readUTF(), + b == REJECTED); + } + } + + @Override + public long serializedSize(Result t, int version) + { + long size = TypeSizes.BYTE_SIZE; + if (t instanceof Success) + { + size += VIntCoding.computeUnsignedVIntSize(serializationVersion.asInt()); + size += Replication.serializer.serializedSize(t.success().replication, serializationVersion); + size += Epoch.serializer.serializedSize(t.success().epoch, serializationVersion); + } + else + { + assert t instanceof Failure; + size += VIntCoding.computeUnsignedVIntSize(((Failure) t).code.value); + size += TypeSizes.sizeof(((Failure)t).message); + } + return size; + } + } + } + + @VisibleForTesting + public static IVerbHandler handlerForTests(Processor processor, Replicator replicator, BiConsumer, InetAddressAndPort> messagingService) + { + return new Handler(processor, replicator, messagingService, () -> LOCAL); + } + + static class Handler implements IVerbHandler + { + private final Processor processor; + private final Replicator replicator; + private final BiConsumer, InetAddressAndPort> messagingService; + private final Supplier cmsStateSupplier; + + Handler(Processor processor, Replicator replicator, Supplier cmsStateSupplier) + { + this(processor, replicator, MessagingService.instance()::send, cmsStateSupplier); + } + + Handler(Processor processor, Replicator replicator, BiConsumer, InetAddressAndPort> messagingService, Supplier cmsStateSupplier) + { + this.processor = processor; + this.replicator = replicator; + this.messagingService = messagingService; + this.cmsStateSupplier = cmsStateSupplier; + } + + public void doVerb(Message message) throws IOException + { + checkCMSState(); + logger.info("Received commit request {} from {}", message.payload, message.from()); + Retry.Deadline retryPolicy = Retry.Deadline.at(message.expiresAtNanos(), new Retry.Jitter(TCMMetrics.instance.commitRetries)); + Result result = processor.commit(message.payload.entryId, message.payload.transform, message.payload.lastKnown, retryPolicy); + if (result.isSuccess()) + { + Result.Success success = result.success(); + replicator.send(success, message.from()); + logger.info("Responding with full result {} to sender {}", result, message.from()); + // TODO: this response message can get lost; how do we re-discover this on the other side? + // TODO: what if we have holes after replaying? + messagingService.accept(message.responseWith(result), message.from()); + } + else + { + Result.Failure failure = result.failure(); + messagingService.accept(message.responseWith(failure), message.from()); + } + } + + private void checkCMSState() + { + switch (cmsStateSupplier.get()) + { + case RESET: + case LOCAL: + break; + case REMOTE: + throw new NotCMSException("Not currently a member of the CMS, can't commit"); + case GOSSIP: + String msg = "Tried to commit when in gossip mode"; + logger.error(msg); + throw new IllegalStateException(msg); + default: + throw new IllegalStateException("Illegal state: " + cmsStateSupplier.get()); + } + } + } + + public interface Replicator + { + Replicator NO_OP = (a,b) -> {}; + void send(Result result, InetAddressAndPort source); + } + + public static class DefaultReplicator implements Replicator + { + private final Supplier directorySupplier; + + public DefaultReplicator(Supplier directorySupplier) + { + this.directorySupplier = directorySupplier; + } + + public void send(Result result, InetAddressAndPort source) + { + if (!result.isSuccess()) + return; + + Result.Success success = result.success(); + Directory directory = directorySupplier.get(); + + // Filter the log entries from the commit result for the purposes of replicating to members of the cluster + // other than the original submitter. We only need to include the sublist of entries starting at the one + // which was newly committed. We exclude entries before that one as the submitter may have been lagging and + // supplied a last known epoch arbitrarily in the past. We include entries after the first newly committed + // one as there may have been a new period automatically triggered and we'd like to push that out to all + // peers too. Of course, there may be other entries interspersed with these but it doesn't harm anything to + // include those too, it may simply be redundant. + Replication newlyCommitted = success.replication.retainFrom(success.epoch); + assert !newlyCommitted.isEmpty() : String.format("Nothing to replicate after retaining epochs since %s from %s", + success.epoch, success.replication); + + for (NodeId peerId : directory.peerIds()) + { + InetAddressAndPort endpoint = directory.endpoint(peerId); + boolean upgraded = directory.version(peerId).isUpgraded(); + // Do not replicate to self and to the peer that has requested to commit this message + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort()) || + (source != null && source.equals(endpoint)) || + !upgraded) + { + continue; + } + + logger.info("Replicating newly committed transformations up to {} to {}", newlyCommitted, endpoint); + MessagingService.instance().send(Message.out(Verb.TCM_REPLICATION, newlyCommitted), endpoint); + } + } + } + +} diff --git a/src/java/org/apache/cassandra/tcm/CurrentEpochRequestHandler.java b/src/java/org/apache/cassandra/tcm/CurrentEpochRequestHandler.java new file mode 100644 index 0000000000..049fdefb0e --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/CurrentEpochRequestHandler.java @@ -0,0 +1,42 @@ +/* + * 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.tcm; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; + +// The cluster metadata equivalent of a Gossip PING; used to exchange the current epochs on +// two peers via a pair of empty messages +public class CurrentEpochRequestHandler implements IVerbHandler +{ + private static final Logger logger = LoggerFactory.getLogger(CurrentEpochRequestHandler.class); + public void doVerb(Message message) throws IOException + { + Message response = message.responseWith(ClusterMetadata.current().epoch); + MessagingService.instance().send(response, message.from()); + // We try to catch up after responding, as watermark request is going to get retried + ClusterMetadataService.instance().fetchLogFromPeerOrCMSAsync(ClusterMetadata.current(), message.from(), message.payload); + } +} diff --git a/src/java/org/apache/cassandra/tcm/Discovery.java b/src/java/org/apache/cassandra/tcm/Discovery.java new file mode 100644 index 0000000000..7bace528af --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Discovery.java @@ -0,0 +1,260 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Uninterruptibles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; + +/** + * Discovery is used to idenitify other participants in the cluster. Nodes send TCM_DISCOVER_REQ + * in several rounds. Node is considered to be discovered by another node when it has responsed + * to its discovery request, or when another node received its discovery request. + */ +public class Discovery +{ + private static final Logger logger = LoggerFactory.getLogger(Discovery.class); + + public static final Discovery instance = new Discovery(); + public static final Serializer serializer = new Serializer(); + + public final IVerbHandler requestHandler; + private final Set discovered = new ConcurrentSkipListSet<>(); + private final AtomicReference state = new AtomicReference<>(State.NOT_STARTED); + + // These two have to be suppliers because during simulation we can send out discovery requests + // rather early, before other node has initialized either MessagingService or DatabaseDescriptor. + // In order to properly route the timeout/failure, Simulator is checking whether the verb is + // response verb, which compares the instance of the verb handler with ResponseHandler. + // This can be improved by refactoring simulator, but this should help meanwhile. + private final Supplier messaging; + private final Supplier> seeds; + + private final InetAddressAndPort self; + + private Discovery() + { + this(MessagingService::instance, DatabaseDescriptor::getSeeds, FBUtilities.getBroadcastAddressAndPort()); + } + + @VisibleForTesting + public Discovery(Supplier messaging, Supplier> seeds, InetAddressAndPort self) + { + this.messaging = messaging; + this.seeds = seeds; + this.requestHandler = new DiscoveryRequestHandler(messaging); + this.self = self; + } + + public DiscoveredNodes discover() + { + return discover(5); + } + + public DiscoveredNodes discover(int rounds) + { + boolean res = state.compareAndSet(State.NOT_STARTED, State.IN_PROGRESS); + assert res : String.format("Can not start discovery as it is in state %s", state.get()); + + long deadline = Clock.Global.nanoTime() + TimeUnit.SECONDS.toNanos(10); + DiscoveredNodes last = null; + int lastCount = discovered.size(); + int unchangedFor = 0; + while (Clock.Global.nanoTime() <= deadline || unchangedFor < rounds) + { + last = discoverOnce(null); + if (last.kind == DiscoveredNodes.Kind.CMS_ONLY) + break; + + if (lastCount == discovered.size()) + unchangedFor++; + else + lastCount = discovered.size(); + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + + res = state.compareAndSet(State.IN_PROGRESS, State.FINISHED); + assert res : String.format("Can not finish discovery as it is in state %s", state.get()); + return last; + } + + public DiscoveredNodes discoverOnce(InetAddressAndPort initiator) + { + Set candidates = new HashSet<>(); + if (initiator != null) + candidates.add(initiator); + else + candidates.addAll(discovered); + + if (candidates.isEmpty()) + candidates.addAll(seeds.get()); + + candidates.remove(self); + + Collection> responses = MessageDelivery.fanoutAndWait(messaging.get(), candidates, Verb.TCM_DISCOVER_REQ, NoPayload.noPayload); + + for (Pair discoveredNodes : responses) + { + if (discoveredNodes.right.kind == DiscoveredNodes.Kind.CMS_ONLY) + return discoveredNodes.right; + + discovered.add(discoveredNodes.left); + discovered.addAll(discoveredNodes.right.nodes); + } + + return new DiscoveredNodes(discovered, DiscoveredNodes.Kind.KNOWN_PEERS); + } + + private final class DiscoveryRequestHandler implements IVerbHandler + { + final Supplier messaging; + + DiscoveryRequestHandler(Supplier messaging) + { + this.messaging = messaging; + } + + @Override + public void doVerb(Message message) + { + Set cms = ClusterMetadata.current().fullCMSMembers(); + logger.debug("Responding to discovery request from {}: {}", message.from(), cms); + + DiscoveredNodes discoveredNodes; + if (!cms.isEmpty()) + discoveredNodes = new DiscoveredNodes(cms, DiscoveredNodes.Kind.CMS_ONLY); + else + { + discovered.add(message.from()); + discoveredNodes = new DiscoveredNodes(new HashSet<>(discovered), DiscoveredNodes.Kind.KNOWN_PEERS); + } + + messaging.get().send(message.responseWith(discoveredNodes), message.from()); + } + } + + public Collection discoveredNodes() + { + return new ArrayList<>(discovered); + } + + public static class DiscoveredNodes + { + private final Set nodes; + private final Kind kind; + + public DiscoveredNodes(Set nodes, Kind kind) + { + this.nodes = nodes; + this.kind = kind; + } + + public Set nodes() + { + return this.nodes; + } + + public Kind kind() + { + return kind; + } + + public String toString() + { + return "DiscoveredNodes{" + + "nodes=" + nodes + + ", kind=" + kind + + '}'; + } + + public enum Kind + { + CMS_ONLY, KNOWN_PEERS + } + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(DiscoveredNodes t, DataOutputPlus out, int version) throws IOException + { + out.write(t.kind.ordinal()); + out.writeUnsignedVInt32(t.nodes.size()); + for (InetAddressAndPort ep : t.nodes) + InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(ep, out, version); + } + + @Override + public DiscoveredNodes deserialize(DataInputPlus in, int version) throws IOException + { + DiscoveredNodes.Kind kind = DiscoveredNodes.Kind.values()[in.readByte()]; + int count = in.readUnsignedVInt32(); + Set eps = Sets.newHashSetWithExpectedSize(count); + for (int i = 0; i < count; i++) + eps.add(InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version)); + return new DiscoveredNodes(eps, kind); + } + + @Override + public long serializedSize(DiscoveredNodes t, int version) + { + int size = TypeSizes.sizeofUnsignedVInt(t.nodes.size()); + size += TypeSizes.BYTE_SIZE; + for (InetAddressAndPort ep : t.nodes) + size += InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(ep, version); + return size; + } + } + + private enum State + { + NOT_STARTED, + IN_PROGRESS, + FINISHED, + FOUND_CMS + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/Epoch.java b/src/java/org/apache/cassandra/tcm/Epoch.java new file mode 100644 index 0000000000..1b7c33a895 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Epoch.java @@ -0,0 +1,202 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +import com.google.common.collect.Sets; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.vint.VIntCoding; + +public class Epoch implements Comparable, Serializable +{ + public static final EpochSerializer serializer = new EpochSerializer(); + public static final IVersionedSerializer messageSerializer = new IVersionedSerializer() + { + @Override + public void serialize(Epoch t, DataOutputPlus out, int version) throws IOException + { + serializer.serialize(t, out); + } + + @Override + public Epoch deserialize(DataInputPlus in, int version) throws IOException + { + return serializer.deserialize(in); + } + + @Override + public long serializedSize(Epoch t, int version) + { + return serializer.serializedSize(t); + } + }; + + public static final Epoch FIRST = new Epoch(1); + public static final Epoch EMPTY = new Epoch(0); + public static final Epoch UPGRADE_STARTUP = new Epoch(Long.MIN_VALUE); + public static final Epoch UPGRADE_GOSSIP = new Epoch(Long.MIN_VALUE + 1); + private static final Set beforeFirst = Sets.newHashSet(EMPTY, UPGRADE_GOSSIP, UPGRADE_STARTUP); + + private final long epoch; + + private Epoch(long epoch) + { + this.epoch = epoch; + } + + public static Epoch create(long epoch) + { + if (epoch == EMPTY.epoch) + return EMPTY; + if (epoch == UPGRADE_GOSSIP.epoch) + return UPGRADE_GOSSIP; + if (epoch == UPGRADE_STARTUP.epoch) + return UPGRADE_STARTUP; + if (epoch == FIRST.epoch) + return FIRST; + return new Epoch(epoch); + } + + public static Epoch max(Epoch l, Epoch r) + { + return l.compareTo(r) > 0 ? l : r; + } + + public boolean isDirectlyBefore(Epoch epoch) + { + if (epoch.equals(Epoch.FIRST)) + return beforeFirst.contains(this); + return this.epoch + 1 == epoch.epoch; + } + + public boolean isDirectlyAfter(Epoch epoch) + { + return epoch.isDirectlyBefore(this); + } + + public Epoch nextEpoch() + { + if (beforeFirst.contains(this)) + return FIRST; + + return new Epoch(epoch + 1); + } + + @Override + public int compareTo(Epoch other) + { + return Long.compare(epoch, other.epoch); + } + + public boolean isBefore(Epoch other) + { + return compareTo(other) < 0; + } + + public boolean isEqualOrBefore(Epoch other) + { + return compareTo(other) <= 0; + } + + public boolean isAfter(Epoch other) + { + return compareTo(other) > 0; + } + + public boolean isEqualOrAfter(Epoch other) + { + return compareTo(other) >= 0; + } + + public boolean is(Epoch other) + { + return equals(other); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Epoch)) return false; + Epoch epoch1 = (Epoch) o; + return epoch == epoch1.epoch; + } + + @Override + public int hashCode() + { + return Objects.hash(epoch); + } + + @Override + public String toString() + { + return "Epoch{" + + "epoch=" + epoch + + '}'; + } + + public long getEpoch() + { + return epoch; + } + + public static class EpochSerializer implements MetadataSerializer + { + // convenience methods for messageSerializer et al + public void serialize(Epoch t, DataOutputPlus out) throws IOException + { + serialize(t, out, Version.V0); + } + + public Epoch deserialize(DataInputPlus in) throws IOException + { + return deserialize(in, Version.V0); + } + + public long serializedSize(Epoch t) + { + return serializedSize(t, Version.V0); + } + + public void serialize(Epoch t, DataOutputPlus out, Version version) throws IOException + { + out.writeUnsignedVInt(t.epoch); + } + + public Epoch deserialize(DataInputPlus in, Version version) throws IOException + { + return Epoch.create(in.readUnsignedVInt()); + } + + public long serializedSize(Epoch t, Version version) + { + return VIntCoding.computeUnsignedVIntSize(t.epoch); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/EpochAwareDebounce.java b/src/java/org/apache/cassandra/tcm/EpochAwareDebounce.java new file mode 100644 index 0000000000..5621845487 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/EpochAwareDebounce.java @@ -0,0 +1,82 @@ +/* + * 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.tcm; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Future; + +/** + * When debouncing from a replica we know exactly which epoch we need, so to avoid retries we + * keep track of which epoch we are currently debouncing, and if a request for a newer epoch + * comes in, we create a new future. If a request for a newer epoch comes in, we simply + * swap out the current future reference for a new one which is requesting the newer epoch. + */ +public class EpochAwareDebounce +{ + public static final EpochAwareDebounce instance = new EpochAwareDebounce<>(); + + private final AtomicReference> currentFuture = new AtomicReference<>(); + private final ExecutorPlus executor; + + private EpochAwareDebounce() + { + // 2 threads since we might start a new debounce for a newer epoch while the old one is executing + this.executor = ExecutorFactory.Global.executorFactory().pooled("debounce", 2); + } + + public Future getAsync(Callable get, Epoch epoch) + { + while (true) + { + EpochAwareAsyncPromise running = currentFuture.get(); + if (running != null && !running.isDone() && running.epoch.isEqualOrAfter(epoch)) + return running; + + EpochAwareAsyncPromise promise = new EpochAwareAsyncPromise<>(epoch); + if (currentFuture.compareAndSet(running, promise)) + { + executor.submit(() -> { + try + { + promise.setSuccess(get.call()); + } + catch (Throwable t) + { + promise.setFailure(t); + } + }); + return promise; + } + } + } + + private static class EpochAwareAsyncPromise extends AsyncPromise + { + private final Epoch epoch; + public EpochAwareAsyncPromise(Epoch epoch) + { + this.epoch = epoch; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/FetchCMSLog.java b/src/java/org/apache/cassandra/tcm/FetchCMSLog.java new file mode 100644 index 0000000000..d669f6249a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/FetchCMSLog.java @@ -0,0 +1,121 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.function.BiFunction; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.utils.FBUtilities; + +public class FetchCMSLog +{ + public static final Serializer serializer = new Serializer(); + + public final Epoch lowerBound; + public final boolean consistentFetch; + + public FetchCMSLog(Epoch lowerBound, boolean consistentFetch) + { + this.lowerBound = lowerBound; + this.consistentFetch = consistentFetch; + } + + public String toString() + { + return "FetchCMSLog{" + + "consistentFetch=" + consistentFetch + + ", lowerBound=" + lowerBound + + '}'; + } + + static class Serializer implements IVersionedSerializer + { + + public void serialize(FetchCMSLog t, DataOutputPlus out, int version) throws IOException + { + Epoch.serializer.serialize(t.lowerBound, out); + out.writeBoolean(t.consistentFetch); + } + + public FetchCMSLog deserialize(DataInputPlus in, int version) throws IOException + { + Epoch epoch = Epoch.serializer.deserialize(in); + boolean consistentFetch = in.readBoolean(); + return new FetchCMSLog(epoch, consistentFetch); + } + + public long serializedSize(FetchCMSLog t, int version) + { + return Epoch.serializer.serializedSize(t.lowerBound) + + TypeSizes.BOOL_SIZE; + } + } + + static class Handler implements IVerbHandler + { + private static final Logger logger = LoggerFactory.getLogger(Handler.class); + + /** + * Receives epoch we need to fetch state up to, and a boolean specifying whether the consistency can be downgraded + * to node-local (which only relevant in cases of CMS expansions/shrinks, and can only be requested by the + * CMS node that collects the highest epoch from the quorum of peers). + */ + private final BiFunction logStateSupplier; + + public Handler() + { + this(DistributedMetadataLogKeyspace::getLogState); + } + + public Handler(BiFunction logStateSupplier) + { + this.logStateSupplier = logStateSupplier; + } + + public void doVerb(Message message) throws IOException + { + FetchCMSLog request = message.payload; + + logger.trace("Received log fetch request {} from {}: start = {}, current = {}", request, message.from(), message.payload.lowerBound, ClusterMetadata.current().epoch); + if (request.consistentFetch && !ClusterMetadataService.instance().isCurrentMember(FBUtilities.getBroadcastAddressAndPort())) + throw new NotCMSException("This node is not in the CMS, can't generate a consistent log fetch response to " + message.from()); + + // If both we and the other node believe it should be caught up with a linearizable read + boolean consistentFetch = request.consistentFetch && !ClusterMetadataService.instance().isCurrentMember(message.from()); + + LogState delta = logStateSupplier.apply(message.payload.lowerBound, consistentFetch); + TCMMetrics.instance.cmsLogEntriesServed(message.payload.lowerBound, delta.latestEpoch()); + logger.info("Responding to {}({}) with log delta: {}", message.from(), request, delta); + MessagingService.instance().send(message.responseWith(delta), message.from()); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/FetchPeerLog.java b/src/java/org/apache/cassandra/tcm/FetchPeerLog.java new file mode 100644 index 0000000000..d104ed7944 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/FetchPeerLog.java @@ -0,0 +1,90 @@ +/* + * 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.tcm; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.LogStorage; + +public class FetchPeerLog +{ + public static final Serializer serializer = new Serializer(); + + public final Epoch start; + + public FetchPeerLog(Epoch start) + { + this.start = start; + } + + public String toString() + { + return "FetchPeerLog{" + + ", start=" + start + + '}'; + } + + static class Serializer implements IVersionedSerializer + { + + public void serialize(FetchPeerLog t, DataOutputPlus out, int version) throws IOException + { + Epoch.serializer.serialize(t.start, out); + } + + public FetchPeerLog deserialize(DataInputPlus in, int version) throws IOException + { + Epoch epoch = Epoch.serializer.deserialize(in); + return new FetchPeerLog(epoch); + } + + public long serializedSize(FetchPeerLog t, int version) + { + return Epoch.serializer.serializedSize(t.start); + } + } + + public static class Handler implements IVerbHandler + { + public static Handler instance = new Handler(); + private static final Logger logger = LoggerFactory.getLogger(Handler.class); + + public void doVerb(Message message) throws IOException + { + FetchPeerLog request = message.payload; + + logger.info("Received peer log fetch request {} from {}: start = {}, current = {}", request, message.from(), message.payload.start, ClusterMetadata.current().epoch); + LogState delta = LogStorage.SystemKeyspace.getLogState(message.payload.start); + TCMMetrics.instance.peerLogEntriesServed(message.payload.start, delta.latestEpoch()); + logger.info("Responding with log delta: {}", delta); + MessagingService.instance().send(message.responseWith(delta), message.from()); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/MetadataKey.java b/src/java/org/apache/cassandra/tcm/MetadataKey.java new file mode 100644 index 0000000000..ab5bbbb0da --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/MetadataKey.java @@ -0,0 +1,81 @@ +/* + * 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.tcm; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class MetadataKey +{ + public static final Serializer serializer = new Serializer(); + + public final String id; + + public MetadataKey(String id) + { + this.id = id; + } + + @Override + public String toString() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof MetadataKey)) return false; + MetadataKey that = (MetadataKey) o; + return id.equals(that.id); + } + + @Override + public int hashCode() + { + return id.hashCode(); + } + + public static final class Serializer implements MetadataSerializer + { + @Override + public void serialize(MetadataKey t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.id); + } + + @Override + public MetadataKey deserialize(DataInputPlus in, Version version) throws IOException + { + return new MetadataKey(in.readUTF()); + } + + @Override + public long serializedSize(MetadataKey t, Version version) + { + return TypeSizes.sizeof(t.id); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/MetadataKeys.java b/src/java/org/apache/cassandra/tcm/MetadataKeys.java new file mode 100644 index 0000000000..fda509186b --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/MetadataKeys.java @@ -0,0 +1,55 @@ +/* + * 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.tcm; + +import java.util.Locale; + +import com.google.common.collect.ImmutableSet; + +public class MetadataKeys +{ + public static final String CORE_NS = MetadataKeys.class.getPackage().getName().toLowerCase(Locale.ROOT); + + public static final MetadataKey SCHEMA = make(CORE_NS, "schema", "dist_schema"); + public static final MetadataKey NODE_DIRECTORY = make(CORE_NS, "membership", "node_directory"); + public static final MetadataKey TOKEN_MAP = make(CORE_NS, "ownership", "token_map"); + public static final MetadataKey DATA_PLACEMENTS = make(CORE_NS, "ownership", "data_placements"); + public static final MetadataKey LOCKED_RANGES = make(CORE_NS, "sequences", "locked_ranges"); + public static final MetadataKey IN_PROGRESS_SEQUENCES = make(CORE_NS, "sequences", "in_progress"); + + public static final ImmutableSet CORE_METADATA = ImmutableSet.of(SCHEMA, + NODE_DIRECTORY, + TOKEN_MAP, + DATA_PLACEMENTS, + LOCKED_RANGES, + IN_PROGRESS_SEQUENCES); + + public static MetadataKey make(String...parts) + { + assert parts != null && parts.length >= 1; + StringBuilder b = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) + { + b.append('.'); + b.append(parts[i]); + } + return new MetadataKey(b.toString()); + } + +} diff --git a/src/java/org/apache/cassandra/tcm/MetadataSnapshots.java b/src/java/org/apache/cassandra/tcm/MetadataSnapshots.java new file mode 100644 index 0000000000..e2491d0d59 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/MetadataSnapshots.java @@ -0,0 +1,121 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public interface MetadataSnapshots +{ + Logger logger = LoggerFactory.getLogger(MetadataSnapshots.class); + + ClusterMetadata getLatestSnapshotAfter(Epoch epoch); + ClusterMetadata getSnapshot(Epoch epoch); + void storeSnapshot(ClusterMetadata metadata); + + static ByteBuffer toBytes(ClusterMetadata metadata) throws IOException + { + Version serializationVersion = Version.minCommonSerializationVersion(); + long serializedSize = VerboseMetadataSerializer.serializedSize(ClusterMetadata.serializer, metadata, serializationVersion); + ByteBuffer bytes = ByteBuffer.allocate((int) serializedSize); + try (DataOutputBuffer dob = new DataOutputBuffer(bytes)) + { + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, metadata, dob, serializationVersion); + } + bytes.flip().rewind(); + return bytes; + } + + @SuppressWarnings("resource") + static ClusterMetadata fromBytes(ByteBuffer serialized) throws IOException + { + if (serialized == null) + return null; + + return VerboseMetadataSerializer.deserialize(ClusterMetadata.serializer, + new DataInputBuffer(serialized, false)); + } + + + MetadataSnapshots NO_OP = new NoOp(); + + public class NoOp implements MetadataSnapshots + { + @Override + public ClusterMetadata getLatestSnapshotAfter(Epoch epoch) + { + return null; + } + + @Override + public ClusterMetadata getSnapshot(Epoch epoch) + { + return null; + } + + @Override + public void storeSnapshot(ClusterMetadata metadata) {} + } + + class SystemKeyspaceMetadataSnapshots implements MetadataSnapshots + { + @Override + public ClusterMetadata getLatestSnapshotAfter(Epoch epoch) + { + Sealed sealed = Sealed.lookupForSnapshot(epoch); + return sealed.epoch.isAfter(epoch) ? getSnapshot(sealed.epoch) : null; + } + + @Override + public ClusterMetadata getSnapshot(Epoch epoch) + { + try + { + return fromBytes(SystemKeyspace.getSnapshot(epoch)); + } + catch (IOException e) + { + logger.error("Could not load snapshot", e); + return null; + } + } + + @Override + public void storeSnapshot(ClusterMetadata metadata) + { + try + { + SystemKeyspace.storeSnapshot(metadata.epoch, metadata.period, toBytes(metadata)); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/MetadataValue.java b/src/java/org/apache/cassandra/tcm/MetadataValue.java new file mode 100644 index 0000000000..ca01b23389 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/MetadataValue.java @@ -0,0 +1,25 @@ +/* + * 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.tcm; + +public interface MetadataValue +{ + V withLastModified(Epoch epoch); + Epoch lastModified(); +} diff --git a/src/java/org/apache/cassandra/tcm/MultiStepOperation.java b/src/java/org/apache/cassandra/tcm/MultiStepOperation.java new file mode 100644 index 0000000000..b315aa9b42 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/MultiStepOperation.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm; + +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.sequences.Move; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.sequences.SequenceState; +import org.apache.cassandra.tcm.sequences.ProgressBarrier; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; + +/** + * Represents a multi-step process performed in order to transition the cluster to some state. + * + * For example, in order to join, the joining node has to execute the following steps: + * * PrepareJoin, which introduces node's tokens, but makes no changes to range ownership, and creates BootstrapAndJoin + * in-progress sequence + * * StartJoin, which adds the bootstrapping node to the write placements for the ranges it gains + * * MidJoin, which adds the bootstrapping node to the read placements for the ranges it has gained, and removes + * owners of these ranges from the read placements + * * FinishJoin, which removes owners of the gained ranges from the write placements. + * + * A multi-step operation necessarily holds all data required to execute the next step in sequence. All in-progress + * operation can be interleaved, as long as their internal logic permits. In other words, an arbitrary number of join, + * move, leave, and replace sequences can be in flight at any point in time, as long as they operate on non-overlapping + * ranges. + * + * It is assumed that the node may crash after committing any of the steps. {@link MultiStepOperation#executeNext()} + * should be implemented such that commits are done _after_ executing necessary prerequisites. For example, + * streaming has to finish before MidJoin transformation (which adds the node to the read placements) is executed. + */ +public abstract class MultiStepOperation +{ + public enum Kind + { + @Deprecated(since = "CEP-21") + JOIN_OWNERSHIP_GROUP(AddToCMS.serializer), + + JOIN(BootstrapAndJoin.serializer), + MOVE(Move.serializer), + REPLACE(BootstrapAndReplace.serializer), + LEAVE(UnbootstrapAndLeave.serializer), + REMOVE(UnbootstrapAndLeave.serializer), + + RECONFIGURE_CMS(ReconfigureCMS.serializer) + ; + + public final AsymmetricMetadataSerializer, ? extends MultiStepOperation> serializer; + + Kind(AsymmetricMetadataSerializer, ? extends MultiStepOperation> serializer) + { + this.serializer = serializer; + } + } + + /** + * Opaque key for identifying an operation when stored in ClusterMetadata's map of in-progress operations + **/ + public interface SequenceKey + { + } + + // Represents the position in the sequence of the next step to be executed. + public final int idx; + // The epoch representing the last modification made by this sequence. Before executing a step in a sequence, + // the node driving it will typically wait for this epoch to be acknowledged by the relevant peers. This helps + // ensure step execution is coordinated across those peers. + public final Epoch latestModification; + + protected MultiStepOperation(int currentStep, Epoch latestModification) + { + this.idx = currentStep; + this.latestModification = latestModification; + } + + /** + * Unique identifier for the type of operation, e.g. JOIN, LEAVE, MOVE + * @return the specific kind of this operation + */ + public abstract Kind kind(); + + /** + * The key under which this operation is stored in {@link org.apache.cassandra.tcm.ClusterMetadata#inProgressSequences} + * In many cases, this is a NodeId, restricting each peer to be the object of at most one bootstrap, token movement, + * decommission, etc at a time. + * @return the key to identify this sequence + */ + protected abstract SequenceKey sequenceKey(); + + /** + * Provides the means to write this sequence's key as bytes. Used when serializing the map of current operations + * {@link org.apache.cassandra.tcm.ClusterMetadata#inProgressSequences} for snapshots and replication. + * @return MetadataSerializer for the sequence's key + */ + public abstract MetadataSerializer keySerializer(); + + /** + * Returns the {@link Transformation.Kind} of the next step due to be executed in the sequence. Used when executing + * a {@link Transformation} which is part of a sequence (specifically, subclasses of + * {@link org.apache.cassandra.tcm.transformations.ApplyPlacementDeltas}) to validate that it is being applied at + * the correct point (i.e. that the type of the transform matches the expected next) + * matches the If all steps + * have already been executed, throws {@code IllegalStateException} + * @return the kind of the next step to be executed. + */ + public abstract Transformation.Kind nextStep(); + + /** + * Executes the next step in the operation. This should usually include a Transformation to mutate ClusterMetadata + * state, and _may_ also involve additional non-metadata operations such as streaming of SSTables to or from peers + * (i.e. in the sequences implementing bootstrap, decommission, etc). + * + * Returns an indication of the sequence's state based on the outcome of executing the step. + * This may express that the operation can continue processing (executing further steps), report a fatal and + * non-fatal errors, or indicate that further execution is blocked while waiting for acknowledgement of preceding + * steps from peers. + * @return sequence state following attempted execution + */ + public abstract SequenceState executeNext(); + + /** + * Advance the state of an in-progress operation after successfully executing a step. Essentially, this "bumps the + * pointer" into the list (actual or logical) of steps which comprise the operation. It is most commonly called by + * Transformations which represent the steps of the operation. For example, in an operation X comprising steps A, B, C + * each step will typically modify ClusterMetadata such that the persisted representation of X indicates what the + * next step to execute is. So at the start, A is the next step and X's internal state will indicate this. Part of + * A's execution is to update that state to show that B is now the next step to run. + * + * The type parameter here represents an entity which can provide any state necessary to perform this advancement. + * In cases where the entire sequence can be constructed a priori (as all the steps are known up front), this may + * simply be the Epoch in which the step was executed. In more dynamic sequences, this parameter may express more + * information. e.g. in the case of CMS reconfiguration, only a single step is known at a time and the paramter + * supplied here is itself the next step. + * + * @param context required to move the sequence's state onto the next step + * @return Logically this sequence, ready to execute the next step. + */ + public abstract MultiStepOperation advance(CONTEXT context); + + /** + * When execution of steps needs to be coordinated across nodes in the cluster, ProgressBarrier enables the operation + * to wait for a quorum of peers to acknowledge an epoch before proceeding. For example, during a BootstrapAndJoin + * sequence, a quorum of relevant nodes must acknowledge that the joining node has become a write replica (which is + * an effect of the StartJoin transformation), before commencing the next step, which includes streaming existing + * data from peers and making the joining node a read replica. + * @return the barrier to wait on before executing the next step of the operation. + */ + public abstract ProgressBarrier barrier(); + + /** + * Reverts any metadata changes that this operation has made up to now. Used to cancel in flight operations such as + * bootstrapping. This is performed by the CancelInProgressSequence transformation, which is also responsible for + * removing the sequence itself from the map in ClusterMetadata. + * @param metadata current cluster metadata. Any metadata changes already committed by this sequence will be + * reverted and the resulting metadata returned + * @return the supplied metadata with this sequence's previously applied changes reverted + */ + public ClusterMetadata.Transformer cancel(ClusterMetadata metadata) + { + throw new UnsupportedOperationException(); + } + + public String status() + { + return "kind: " + kind() + ", current step: " + idx + ", barrier: " + barrier(); + } +} diff --git a/test/unit/org/apache/cassandra/service/StorageServiceAccessor.java b/src/java/org/apache/cassandra/tcm/NotCMSException.java similarity index 75% rename from test/unit/org/apache/cassandra/service/StorageServiceAccessor.java rename to src/java/org/apache/cassandra/tcm/NotCMSException.java index a45f81d344..48bf6021fa 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceAccessor.java +++ b/src/java/org/apache/cassandra/tcm/NotCMSException.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -16,14 +16,12 @@ * limitations under the License. */ -package org.apache.cassandra.service; +package org.apache.cassandra.tcm; -import org.apache.cassandra.locator.TokenMetadata; - -public class StorageServiceAccessor +public class NotCMSException extends RuntimeException { - public static TokenMetadata setTokenMetadata(TokenMetadata tmd) + public NotCMSException(String format) { - return StorageService.instance.setTokenMetadataUnsafe(tmd); + super(format); } } diff --git a/src/java/org/apache/cassandra/tcm/PaxosBackedProcessor.java b/src/java/org/apache/cassandra/tcm/PaxosBackedProcessor.java new file mode 100644 index 0000000000..3c1bb3c306 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/PaxosBackedProcessor.java @@ -0,0 +1,219 @@ +/* + * 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.tcm; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.exceptions.ReadTimeoutException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallbackWithFailure; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Promise; + +import static org.apache.cassandra.schema.DistributedMetadataLogKeyspace.tryCommit; + +public class PaxosBackedProcessor extends AbstractLocalProcessor +{ + private static final Logger logger = LoggerFactory.getLogger(PaxosBackedProcessor.class); + + public PaxosBackedProcessor(LocalLog log) + { + super(log); + } + + @Override + protected boolean tryCommitOne(Entry.Id entryId, Transformation transform, + Epoch previousEpoch, Epoch nextEpoch, + long previousPeriod, long nextPeriod, boolean sealPeriod) + { + return tryCommit(entryId, transform, previousEpoch, nextEpoch, previousPeriod, nextPeriod, sealPeriod); + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + ClusterMetadata metadata = log.waitForHighestConsecutive(); + + // We can perform a local-only read without going through paxos subsystem in case of a single CMS node. + if (metadata.fullCMSMembers().size() > 1) + { + try + { + // Attempt to perform a consistent fetch + log.append(DistributedMetadataLogKeyspace.getLogState(metadata.epoch, true)); + return log.waitForHighestConsecutive(); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + TCMMetrics.instance.fetchCMSLogConsistencyDowngrade.mark(); + logger.warn("Could not perform consistent fetch, downgrading to fetching from CMS peers.", t); + } + } + Set replicas = metadata.fullCMSMembersAsReplicas(); + + // We prefer to always perform a consistent fetch (i.e. Paxos read of the distributed log state table). + // However, in some cases (specifically, during CMS membership changes) this may not be possible, as Paxos + // relies on matching Participants, and there might be a mismatch during the membership change. In such + // case, we allow inconsistent fetch. In other words, replay from local log of the majority of the CMS replicas. + int blockFor = replicas.size() == 1 ? 1 : (replicas.size() / 2) + 1; + + Set collected = new HashSet<>(blockFor); + Set requests = new HashSet<>(); + AtomicReference highestSeen = new AtomicReference<>(metadata.epoch); + + for (Replica peer : replicas) + requests.add(new FetchLogRequest(peer, MessagingService.instance(), metadata.epoch)); + + while (!retryPolicy.reachedMax()) + { + Iterator iter = requests.iterator(); + boolean hasRequestToSelf = false; + while (iter.hasNext()) + { + FetchLogRequest request = iter.next(); + if (request.to.isSelf()) + { + hasRequestToSelf = true; + iter.remove(); + } + else + { + request.retry(); + } + } + + // Fire off a blocking request to self only after dispatching requests to other participants + if (hasRequestToSelf) + { + log.append(DistributedMetadataLogKeyspace.getLogState(metadata.epoch, false)); + collected.add(FBUtilities.getBroadcastAddressAndPort()); + } + + iter = requests.iterator(); + long nextTimeout = Math.min(retryPolicy.deadlineNanos, Clock.Global.nanoTime() + DatabaseDescriptor.getRpcTimeout(TimeUnit.NANOSECONDS)); + while (iter.hasNext()) + { + FetchLogRequest request = iter.next(); + if (request.condition.awaitUninterruptibly(Math.max(0, nextTimeout - Clock.Global.nanoTime()), TimeUnit.NANOSECONDS) && + request.condition.isSuccess()) + { + collected.add(request.to.endpoint()); + LogState logState = unwrap(request.condition); + log.append(logState); + highestSeen.getAndUpdate(o -> { + if (o == null || logState.latestEpoch().isAfter(o)) + return logState.latestEpoch(); + return o; + }); + iter.remove(); + } + } + + if (collected.size() < blockFor) + { + retryPolicy.maybeSleep(); + continue; + } + + Epoch highest = highestSeen.get(); + TCMMetrics.instance.cmsLogEntriesFetched(metadata.epoch, highest); + assert waitFor == null || highest.isEqualOrAfter(waitFor) : String.format("%s should have been higher than waited for epoch %s", highestSeen, waitFor); + return log.waitForHighestConsecutive(); + } + + TCMMetrics.instance.cmsLogEntriesFetched(metadata.epoch, highestSeen.get()); + throw new ReadTimeoutException(ConsistencyLevel.QUORUM, blockFor - collected.size(), blockFor, false); + } + + private static T unwrap(Promise promise) + { + if (!promise.isDone() || !promise.isSuccess()) + throw new IllegalStateException("Can only unwrap an already done promise."); + try + { + return promise.get(); + } + catch (InterruptedException | ExecutionException e) + { + throw new IllegalStateException("Promise shoulde not have thrown", e); + } + } + + private static class FetchLogRequest implements RequestCallbackWithFailure + { + private AsyncPromise condition = null; + private final Replica to; + private final MessageDelivery messagingService; + private final FetchCMSLog request; + + public FetchLogRequest(Replica to, MessageDelivery messagingService, Epoch lowerBound) + { + this.to = to; + this.messagingService = messagingService; + this.request = new FetchCMSLog(lowerBound, false); + } + + @Override + public void onResponse(Message msg) + { + condition.trySuccess(msg.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + logger.debug("Error response from {} with {}", from, failureReason); + condition.tryFailure(new TimeoutException(failureReason.toString())); + } + + public void retry() + { + condition = new AsyncPromise<>(); + messagingService.sendWithCallback(Message.out(Verb.TCM_FETCH_CMS_LOG_REQ, request), to.endpoint(), this); + } + } + + +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/PeerLogFetcher.java b/src/java/org/apache/cassandra/tcm/PeerLogFetcher.java new file mode 100644 index 0000000000..29c7e6b1cb --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/PeerLogFetcher.java @@ -0,0 +1,108 @@ +/* + * 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.tcm; + +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Timer; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.concurrent.Future; + +public class PeerLogFetcher +{ + private static final Logger logger = LoggerFactory.getLogger(RemoteProcessor.class); + private final LocalLog log; + + public PeerLogFetcher(LocalLog log) + { + this.log = log; + } + + /** + * fetch log entries from the given remote, we have already seen a message from this replica with epoch awaitAtleast. + */ + public ClusterMetadata fetchLogEntriesAndWait(InetAddressAndPort remote, Epoch awaitAtleast) + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (metadata.epoch.isEqualOrAfter(awaitAtleast)) + return metadata; + + try + { + return asyncFetchLog(remote, awaitAtleast).get(DatabaseDescriptor.getRpcTimeout(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) + { + throw new RuntimeException("Can not fetch log entries during shutdown", e); + } + catch (ExecutionException | TimeoutException e) + { + logger.warn("Could not fetch log entries from peer, remote = {}, await = {}", remote, awaitAtleast); + } + return metadata; + } + + public Future asyncFetchLog(InetAddressAndPort remote, Epoch awaitAtleast) + { + return EpochAwareDebounce.instance.getAsync(() -> fetchLogEntriesAndWaitInternal(remote, awaitAtleast), awaitAtleast); + } + + private ClusterMetadata fetchLogEntriesAndWaitInternal(InetAddressAndPort remote, Epoch awaitAtleast) + { + Epoch before = ClusterMetadata.current().epoch; + if (before.isEqualOrAfter(awaitAtleast)) + return ClusterMetadata.current(); + + logger.info("Fetching log from {}, at least {}", remote, awaitAtleast); + + try (Timer.Context ctx = TCMMetrics.instance.fetchPeerLogLatency.time()) + { + LogState logState = RemoteProcessor.sendWithCallback(Verb.TCM_FETCH_PEER_LOG_REQ, + new FetchPeerLog(before), + new RemoteProcessor.CandidateIterator(Collections.singletonList(remote)), + Retry.Deadline.after(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.NANOSECONDS), + new Retry.Jitter(TCMMetrics.instance.fetchLogRetries))); + log.append(logState); + ClusterMetadata fetched = log.waitForHighestConsecutive(); + if (fetched.epoch.isEqualOrAfter(awaitAtleast)) + { + TCMMetrics.instance.peerLogEntriesFetched(before, logState.latestEpoch()); + return fetched; + } + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.warn("Unable to fetch log entries from " + remote, t); + } + return ClusterMetadata.current(); + } +} diff --git a/src/java/org/apache/cassandra/tcm/Period.java b/src/java/org/apache/cassandra/tcm/Period.java new file mode 100644 index 0000000000..32bf046232 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Period.java @@ -0,0 +1,207 @@ +/* + * 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.tcm; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongConsumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; + +public class Period +{ + private static final Logger logger = LoggerFactory.getLogger(Period.class); + public static final long EMPTY = 0; + public static final long FIRST = 1; + + /** + * Last resort fallback to find where in the log table (either local or distributed) we can find a + * given epoch. If the current ClusterMetadata.period > Period.FIRST (as should be the case normally), + * we start there and walk backwards through the log table. Otherwise, we walk forwards. + * Note, this method is only used (and should only be used) as a last resort in case the local index of + * max epoch to period (in system.metadata_sealed_periods) is not available. + * @param logTable which table to scan, system.local_metadata_log or system_cluster_metadata.distributed_metadata_log + * @param since the target epoch + * @return the period at which to begin reading when contstructing a list of log entries which follow the + * target epoch + */ + public static long scanLogForPeriod(TableMetadata logTable, Epoch since) + { + long currentPeriod = ClusterMetadata.current().period; + PeriodFinder visitor = currentPeriod > Period.FIRST + ? new ReversePeriodFinder(since, currentPeriod) + : new ForwardPeriodFinder(since); + scan(logTable, visitor); + return Math.max(visitor.readPeriod, Period.FIRST); + } + + /** + * Scan the log table (could be either local or distributed, but in practice only the local is used), + * to find the n most recently sealed periods, beginning and including at a given start point. + * Used when initialising the in-memory index of max epoch to sealed period at startup. + * @param logTable which table to scan, system.local_metadata_log or system_cluster_metadata.distributed_metadata_log + * @param startPeriod the period to begin reading from + * @param max maximum number of sealed periods to collect + * @return the list of most recently sealed periods, starting from & including startPeriod + */ + public static List scanLogForRecentlySealed(TableMetadata logTable, long startPeriod, int max) + { + ReverseSealedCollector visitor = new ReverseSealedCollector(startPeriod, max); + scan(logTable, visitor); + return visitor.result; + } + + private static void scan(TableMetadata logTable, Visitor visitor) + { + ColumnMetadata col = logTable.getColumn(ColumnIdentifier.getInterned("current_epoch", true)); + ColumnFilter columnFilter = ColumnFilter.selection(RegularAndStaticColumns.of(col)); + ClusteringIndexSliceFilter sliceFilter = new ClusteringIndexSliceFilter(Slices.NONE, false); + long startPeriod = visitor.readPeriod; + long partitionsScanned = 0; + + boolean done = false; + while (!done) + { + DecoratedKey key = logTable.partitioner.decorateKey(ByteBufferUtil.bytes(visitor.readPeriod)); + ReadCommand command = SinglePartitionReadCommand.create(logTable, + FBUtilities.nowInSeconds(), + key, + columnFilter, + sliceFilter); + try (ReadExecutionController executionController = command.executionController(); + PartitionIterator iterator = command.executeInternal(executionController)) + { + if (!iterator.hasNext()) + done = true; + else + { + ++partitionsScanned; + try (RowIterator partition = iterator.next()) + { + visitor.accept(ByteBufferUtil.toLong(partition.staticRow().getCell(col).buffer())); + } + done = visitor.done; + } + } + } + logger.trace("Performed partial local scan of {}.{}, started at period {}, {} partitions scanned", + logTable.keyspace, logTable.name, startPeriod, partitionsScanned); + } + + private static abstract class Visitor implements LongConsumer + { + boolean done = false; + long readPeriod; + Visitor(long startPeriod) + { + readPeriod = startPeriod; + } + } + + private static abstract class PeriodFinder extends Visitor + { + long targetEpoch; + PeriodFinder(Epoch target, long startPeriod) + { + super(startPeriod); + this.readPeriod = startPeriod; + this.targetEpoch = target.getEpoch(); + } + } + + private static class ReversePeriodFinder extends PeriodFinder + { + ReversePeriodFinder(Epoch target, long startPeriod) + { + super(target, startPeriod); + } + + @Override + public void accept(long maxEpochInPeriod) + { + + if (maxEpochInPeriod < targetEpoch) + { + readPeriod++; + done = true; + } + else if (maxEpochInPeriod == targetEpoch) + done = true; + else + --readPeriod; // keep walking backwards + } + } + + private static class ForwardPeriodFinder extends PeriodFinder + { + ForwardPeriodFinder(Epoch target) + { + super(target, Period.FIRST); + } + + @Override + public void accept(long maxEpochInPeriod) + { + if (maxEpochInPeriod > targetEpoch) + done = true; + else + ++readPeriod; // keep walking forwards + } + } + + private static class ReverseSealedCollector extends Visitor + { + int max; + List result; + ReverseSealedCollector(long startPeriod, int max) + { + super(startPeriod); + result = new ArrayList<>(max); + this.max = max; + } + + @Override + public void accept(long maxEpochInPeriod) + { + if (result.size() < max) + result.add(new Sealed(readPeriod, maxEpochInPeriod)); + + --readPeriod; + done = result.size() >= max; + } + } + +} diff --git a/src/java/org/apache/cassandra/tcm/Processor.java b/src/java/org/apache/cassandra/tcm/Processor.java new file mode 100644 index 0000000000..e3f12852c2 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Processor.java @@ -0,0 +1,73 @@ +/* + * 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.tcm; + +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.tcm.log.Entry; + +public interface Processor +{ + /** + * Method is _only_ responsible to commit the transformation to the cluster metadata. Implementers _have to ensure_ + * local visibility and enactment of the metadata! + */ + default Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown) + { + // When the cluster is bounced, it may happen that regular nodes come up earlier than CMS nodes, or CMS + // nodes come up and fail to finish the startup since other CMS nodes are not up yet, and therefore can not + // submit the STARTUP message. This allows the bounces affecting majority of CMS nodes to finish successfully. + if (transform.kind() == Transformation.Kind.STARTUP) + { + return commit(entryId, transform, lastKnown, + Retry.Deadline.retryIndefinitely(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.NANOSECONDS), + TCMMetrics.instance.commitRetries)); + } + + return commit(entryId, transform, lastKnown, + Retry.Deadline.after(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.NANOSECONDS), + new Retry.Jitter(TCMMetrics.instance.commitRetries))); + } + + Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy); + + /** + * Fetches log from CMS up to the highest currently known epoch. + *

    + * After fetching, all items _at least_ up to returned epoch will be visible. + * + * This method deliberately does not necessitate passing an epoch, since it guarantees catching up to the _latest_ + * epoch. Users that require catching up to _at least_ some epoch need to guard this call with a check of whether + * local epoch is already at that point. + */ + default ClusterMetadata fetchLogAndWait() + { + return fetchLogAndWait(null); // wait for the highest possible epoch + } +; + default ClusterMetadata fetchLogAndWait(Epoch waitFor) + { + return fetchLogAndWait(waitFor, + Retry.Deadline.after(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.NANOSECONDS), + new Retry.Jitter(TCMMetrics.instance.fetchLogRetries))); + } + ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy); +} diff --git a/src/java/org/apache/cassandra/tcm/RecentlySealedPeriods.java b/src/java/org/apache/cassandra/tcm/RecentlySealedPeriods.java new file mode 100644 index 0000000000..b04ec56622 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/RecentlySealedPeriods.java @@ -0,0 +1,140 @@ +/* + * 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.tcm; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.config.CassandraRelevantProperties; + +/** + * In-memory lookup of the most recent sealed periods, indexed by their max epoch. + * Rebuilt at startup from system.metadata_sealed_periods and updated whenever a + * SealPeriod is enacted. Used in LogReader::getReplication to identify the period + * to start from when constructing a list of log entries since a given epoch. If + * the target is outside the range of this index, we eventually fall back to a read + * from the system.metadata_sealed_periods table. + */ +public class RecentlySealedPeriods +{ + private static final Sealed[] EMPTY_ARRAY = new Sealed[0]; + public static final RecentlySealedPeriods EMPTY = new RecentlySealedPeriods(EMPTY_ARRAY); + private int maxSize = CassandraRelevantProperties.TCM_RECENTLY_SEALED_PERIOD_INDEX_SIZE.getInt(); + private Sealed[] recent; + + private RecentlySealedPeriods(Sealed first) + { + this.recent = new Sealed[]{first}; + } + + private RecentlySealedPeriods(Sealed[] recent) + { + this.recent = recent; + } + + public static RecentlySealedPeriods init(List recent) + { + Collections.sort(recent); + return new RecentlySealedPeriods(recent.toArray(new Sealed[recent.size()])); + } + + public RecentlySealedPeriods with(Epoch epoch, long period) + { + if (recent == null) + { + return new RecentlySealedPeriods(new Sealed(period, epoch)); + } + else + { + int toCopy = Math.min(recent.length, maxSize - 1); + int newSize = Math.min(recent.length + 1, maxSize); + Sealed[] newList = new Sealed[newSize]; + System.arraycopy(recent, recent.length - toCopy, newList, 0, toCopy); + newList[newSize - 1] = new Sealed(period, epoch); + // shouldn't be necessary, but is cheap + Arrays.sort(newList, Sealed::compareTo); + return new RecentlySealedPeriods(newList); + } + } + + /** + * Find the highest sealed period *after* the target epoch. When catching + * up or replaying the log, we want the most recent snapshot available, + * as long as its epoch is greater than the target. + * If the target epoch is greater than the max epoch in the latest sealed + * period, then assume there is no suitable snapshot. + * @param epoch + * @return + */ + public Sealed lookupEpochForSnapshot(Epoch epoch) + { + // if the target is > the highest indexed value there's no need to + // scan the index. Instead, just signal to the caller that no suitable + // sealed period was found. + if (recent.length > 0) + { + Sealed latest = recent[recent.length - 1]; + return latest.epoch.isAfter(epoch) ? latest : Sealed.EMPTY; + } + return Sealed.EMPTY; + } + + // TODO add a lookupEpochForSnapshotBetween(start, end) so we can walk + // through the index if the latest snapshot happens to be missing + + /** + * Find the *closest* sealed period to a target epoch. Used to identify + * the period to start at if building a list of all log entries with an + * epoch greater than the one supplied. If the target epoch happens to be + * the max in a sealed period, we would start with the period following + * that. If the target epoch is equal to or after the max in the latest + * sealed period, assume that the period following that has yet to be sealed. + * @param epoch + * @return + */ + public Sealed lookupPeriodForReplication(Epoch epoch) + { + // if the target is > the highest indexed value there's no need to + // scan the index. Instead, just signal to the caller that no suitable + // sealed period was found. + if (recent.length > 0 && epoch.isEqualOrAfter(recent[recent.length - 1].epoch)) + return Sealed.EMPTY; + + for (int i = recent.length - 1; i >= 0; i--) + { + Sealed e = recent[i]; + if (e.epoch.isEqualOrBefore(epoch)) + return recent[Math.min(i + 1, recent.length - 1)]; + } + + // the target epoch couldn't be found in the index, signal to the + // caller that a slower/more expensive/more comprehensive lookup + // is required + return Sealed.EMPTY; + } + + @VisibleForTesting + public Sealed[] array() + { + return recent; + } +} diff --git a/src/java/org/apache/cassandra/tcm/RemoteProcessor.java b/src/java/org/apache/cassandra/tcm/RemoteProcessor.java new file mode 100644 index 0000000000..6db8de2020 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/RemoteProcessor.java @@ -0,0 +1,341 @@ +/* + * 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.tcm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Timer; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallbackWithFailure; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.Discovery.DiscoveredNodes; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.Promise; + +import static org.apache.cassandra.exceptions.ExceptionCode.SERVER_ERROR; +import static org.apache.cassandra.net.NoPayload.noPayload; +import static org.apache.cassandra.tcm.ClusterMetadataService.State.REMOTE; + +public final class RemoteProcessor implements Processor +{ + private static final Logger logger = LoggerFactory.getLogger(RemoteProcessor.class); + private final Supplier> discoveryNodes; + private final LocalLog log; + + RemoteProcessor(LocalLog log, Supplier> discoveryNodes) + { + this.log = log; + this.discoveryNodes = discoveryNodes; + } + + @Override + @SuppressWarnings("resource") + public Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy) + { + try + { + Commit.Result result = sendWithCallback(Verb.TCM_COMMIT_REQ, + new Commit(entryId, transform, lastKnown), + new CandidateIterator(candidates(false)), + retryPolicy); + + if (result.isSuccess()) + { + Commit.Result.Success success = result.success(); + log.append(success.replication.entries()); + log.awaitAtLeast(success.epoch); + } + + return result; + } + catch (Exception e) + { + return new Commit.Result.Failure(SERVER_ERROR, e.getMessage() == null + ? e.getClass().toString() + : e.getMessage(), false); + } + } + + private List candidates(boolean allowDiscovery) + { + List candidates = new ArrayList<>(log.metadata().fullCMSMembers()); + if (candidates.isEmpty()) + candidates.addAll(DatabaseDescriptor.getSeeds()); + // todo: should we add all other nodes, too? + if (candidates.isEmpty() && allowDiscovery) + { + for (InetAddressAndPort discoveryNode : discoveryNodes.get()) + { + if (!discoveryNode.equals(FBUtilities.getBroadcastAddressAndPort())) + candidates.add(discoveryNode); + } + } + + Collections.shuffle(candidates); + + return candidates; + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + // Synchonous, non-debounced call if we are waiting for the highest epoch. Should be used sparingly. + if (waitFor == null) + return fetchLogAndWaitInternal(); + + try + { + return EpochAwareDebounce.instance.getAsync(this::fetchLogAndWaitInternal, waitFor).get(retryPolicy.remainingNanos(), TimeUnit.NANOSECONDS); + } + catch (InterruptedException e) + { + throw new RuntimeException("Can not replay during shutdown", e); + } + catch (ExecutionException | TimeoutException e) + { + throw new RuntimeException("Could not replay", e); + } + } + + private ClusterMetadata fetchLogAndWaitInternal() + { + return fetchLogAndWait(new CandidateIterator(candidates(true), false), log); + } + + public static ClusterMetadata fetchLogAndWait(CandidateIterator candidateIterator, LocalLog log) + { + try (Timer.Context ctx = TCMMetrics.instance.fetchCMSLogLatency.time()) + { + Epoch currentEpoch = log.metadata().epoch; + LogState replay = sendWithCallback(Verb.TCM_FETCH_CMS_LOG_REQ, + new FetchCMSLog(currentEpoch, ClusterMetadataService.state() == REMOTE), + candidateIterator, + new Retry.Backoff(TCMMetrics.instance.fetchLogRetries)); + if (!replay.isEmpty()) + { + logger.info("Replay request returned replay data: {}", replay); + log.append(replay); + TCMMetrics.instance.cmsLogEntriesFetched(currentEpoch, replay.latestEpoch()); + } + + return log.waitForHighestConsecutive(); + } + } + + // todo rename to send with retries or something + public static RSP sendWithCallback(Verb verb, REQ request, CandidateIterator candidates, Retry retryPolicy) + { + try + { + Promise promise = new AsyncPromise<>(); + sendWithCallbackAsync(promise, verb, request, candidates, retryPolicy); + return promise.awaitUninterruptibly().get(); + } + catch (InterruptedException | ExecutionException e) + { + throw new RuntimeException(e); + } + } + + public static void sendWithCallbackAsync(Promise promise, Verb verb, REQ request, CandidateIterator candidates, Retry retryPolicy) + { + class Request implements RequestCallbackWithFailure + { + void retry() + { + if (promise.isCancelled() || promise.isDone()) + return; + if (!candidates.hasNext()) + promise.tryFailure(new IllegalStateException(String.format("Ran out of candidates while sending %s: %s", verb, candidates))); + + MessagingService.instance().sendWithCallback(Message.out(verb, request), candidates.next(), this); + } + + @Override + public void onResponse(Message msg) + { + promise.trySuccess(msg.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason reason) + { + if (reason == RequestFailureReason.NOT_CMS) + { + logger.debug("{} is not a member of the CMS, querying it to discover current membership", from); + DiscoveredNodes cms = tryDiscover(from); + candidates.addCandidates(cms); + candidates.timeout(from); + logger.debug("Got CMS from {}: {}, retrying on: {}", from, cms, candidates); + } + else + { + candidates.timeout(from); + logger.warn("Got error from {}: {} when sending {}, retrying on {}", from, reason, verb, candidates); + } + + if (retryPolicy.reachedMax()) + promise.tryFailure(new IllegalStateException(String.format("Could not succeed sending %s to %s after %d tries", verb, candidates, retryPolicy.tries))); + else + retry(); + } + } + + new Request().retry(); + } + + private static DiscoveredNodes tryDiscover(InetAddressAndPort ep) + { + // TODO: there are no retries here + Promise promise = new AsyncPromise<>(); + MessagingService.instance().sendWithCallback(Message.out(Verb.TCM_DISCOVER_REQ, noPayload), ep, new RequestCallbackWithFailure() + { + @Override + public void onResponse(Message msg) + { + promise.setSuccess(msg.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + // "success" - this lets us just try the next one in cmsIter + promise.setSuccess(new DiscoveredNodes(Collections.emptySet(), DiscoveredNodes.Kind.KNOWN_PEERS)); + } + }); + try + { + return promise.get(DatabaseDescriptor.getCmsAwaitTimeout().to(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + catch (Exception e) + { + logger.warn("Could not discover CMS from " + ep, e); + } + return new DiscoveredNodes(Collections.emptySet(), DiscoveredNodes.Kind.KNOWN_PEERS); + } + + public static class CandidateIterator extends AbstractIterator + { + private final Deque candidates; + private final boolean checkLive; + + @SuppressWarnings("resource") + public CandidateIterator(Collection initialContacts) + { + this(initialContacts, true); + } + + @SuppressWarnings("resource") + public CandidateIterator(Collection initialContacts, boolean checkLive) + { + this.candidates = new ConcurrentLinkedDeque<>(initialContacts); + this.checkLive = checkLive; + } + + /** + * called when we get a response from LOG_DISCOVER_CMS_REQ + * + * @param discoveredNodes + */ + public void addCandidates(DiscoveredNodes discoveredNodes) + { + if (discoveredNodes.kind() == DiscoveredNodes.Kind.CMS_ONLY) + discoveredNodes.nodes().forEach(candidates::addFirst); + else + discoveredNodes.nodes().forEach(candidates::addLast); + } + + public void notCms(InetAddressAndPort resp) + { + candidates.addLast(resp); + } + + public void timeout(InetAddressAndPort timedOut) + { + candidates.addLast(timedOut); + } + + public String toString() + { + return "CandidateIterator{" + + "candidates=" + candidates + + ", checkLive=" + checkLive + + '}'; + } + + public InetAddressAndPort peekLast() + { + return candidates.peekLast(); + } + + @Override + protected InetAddressAndPort computeNext() + { + boolean checkLive = this.checkLive; + InetAddressAndPort first = null; + + while (!candidates.isEmpty()) + { + InetAddressAndPort ep = candidates.pop(); + + // If we've cycled through all candidates, disable liveness check + if (first == null) + first = ep; + else if (first.equals(ep)) + checkLive = false; + + if (checkLive && !FailureDetector.instance.isAlive(ep)) + { + if (candidates.isEmpty()) + return ep; + else + { + candidates.addLast(ep); + continue; + } + } + return ep; + } + return endOfData(); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/Retry.java b/src/java/org/apache/cassandra/tcm/Retry.java new file mode 100644 index 0000000000..6e5b74a8da --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Retry.java @@ -0,0 +1,202 @@ +/* + * 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.tcm; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.Meter; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.utils.Clock; + +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static org.apache.cassandra.tcm.Retry.Jitter.MAX_JITTER_MS; + +public abstract class Retry +{ + protected static final int MAX_TRIES = DatabaseDescriptor.getCmsDefaultRetryMaxTries(); + protected final int maxTries; + protected int tries; + protected Meter retryMeter; + + public Retry(Meter retryMeter) + { + this(MAX_TRIES, retryMeter); + } + + public Retry(int maxTries, Meter retryMeter) + { + this.maxTries = maxTries; + this.retryMeter = retryMeter; + } + + public int currentTries() + { + return tries; + } + + public boolean reachedMax() + { + return tries >= maxTries; + } + + public void maybeSleep() + { + tries++; + retryMeter.mark(); + sleepUninterruptibly(sleepFor(), TimeUnit.MILLISECONDS); + } + + protected abstract long sleepFor(); + + public static class Jitter extends Retry + { + public static final int MAX_JITTER_MS = Math.toIntExact(DatabaseDescriptor.getDefaultRetryBackoff().to(TimeUnit.MILLISECONDS)); + private final Random random; + private final int maxJitterMs; + + public Jitter(Meter retryMeter) + { + this(MAX_TRIES, MAX_JITTER_MS, new Random(), retryMeter); + } + + private Jitter(int maxTries, int maxJitterMs, Random random, Meter retryMeter) + { + super(maxTries, retryMeter); + this.random = random; + this.maxJitterMs = maxJitterMs; + } + + public long sleepFor() + { + int actualBackoff = ThreadLocalRandom.current().nextInt(maxJitterMs / 2, maxJitterMs); + return random.nextInt(actualBackoff); + } + } + + public static class Backoff extends Retry + { + private static final int RETRY_BACKOFF_MS = Math.toIntExact(DatabaseDescriptor.getDefaultRetryBackoff().to(TimeUnit.MILLISECONDS)); + protected final int backoffMs; + + public Backoff(Meter retryMeter) + { + this(MAX_TRIES, RETRY_BACKOFF_MS, retryMeter); + } + + public Backoff(int maxTries, int backoffMs, Meter retryMeter) + { + super(maxTries, retryMeter); + this.backoffMs = backoffMs; + } + + public long sleepFor() + { + return (long) tries * backoffMs; + } + + @Override + public String toString() + { + return "Backoff{" + + "backoffMs=" + backoffMs + + ", maxTries=" + maxTries + + ", tries=" + tries + + '}'; + } + } + + public static class Deadline extends Retry + { + public final long deadlineNanos; + protected final Retry delegate; + + private Deadline(long deadlineNanos, Retry delegate) + { + super(delegate.maxTries, delegate.retryMeter); + assert deadlineNanos > 0 : String.format("Deadline should be strictly positive but was %d.", deadlineNanos); + this.deadlineNanos = deadlineNanos; + this.delegate = delegate; + } + + public static Deadline at(long deadlineNanos, Retry delegate) + { + return new Deadline(deadlineNanos, delegate); + } + + public static Deadline after(long timeoutNanos, Retry delegate) + { + return new Deadline(Clock.Global.nanoTime() + timeoutNanos, delegate); + } + + /** + * Since we are using message expiration for communicating timeouts to CMS nodes, we have to be careful not + * to overflow the long, since messaging is using only 32 bits for deadlines. To achieve that, we are + * giving `timeoutNanos` every time we retry, but will retry indefinitely. + */ + public static Deadline retryIndefinitely(long timeoutNanos, Meter retryMeter) + { + return new Deadline(Clock.Global.nanoTime() + timeoutNanos, + new Retry.Jitter(Integer.MAX_VALUE, MAX_JITTER_MS, new Random(), retryMeter)) + { + @Override + public boolean reachedMax() + { + return false; + } + + @Override + public long remainingNanos() + { + return timeoutNanos; + } + }; + } + + @Override + public boolean reachedMax() + { + return delegate.reachedMax() || Clock.Global.nanoTime() > deadlineNanos; + } + + public long remainingNanos() + { + return Math.max(0, deadlineNanos - Clock.Global.nanoTime()); + } + + @Override + public int currentTries() + { + return delegate.currentTries(); + } + + @Override + public long sleepFor() + { + return delegate.sleepFor(); + } + + public String toString() + { + return String.format("Deadline{remainingMs=%d, tries=%d/%d}", TimeUnit.NANOSECONDS.toMillis(remainingNanos()), currentTries(), delegate.maxTries); + } + } + +} diff --git a/src/java/org/apache/cassandra/tcm/Sealed.java b/src/java/org/apache/cassandra/tcm/Sealed.java new file mode 100644 index 0000000000..875292ff07 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Sealed.java @@ -0,0 +1,128 @@ +/* + * 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.tcm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.SystemKeyspace; + +public class Sealed implements Comparable +{ + private static final Logger logger = LoggerFactory.getLogger(Sealed.class); + + public static final Sealed EMPTY = new Sealed(Period.EMPTY, Epoch.EMPTY); + public final long period; + public final Epoch epoch; + + private static final AtomicReference index = new AtomicReference<>(RecentlySealedPeriods.EMPTY); + + public static Sealed lookupForReplication(Epoch since) + { + Sealed sealed = index.get().lookupPeriodForReplication(since); + if (sealed.equals(EMPTY)) + { + logger.info("No sealed period found in index for epoch {}, querying system table", since); + sealed = SystemKeyspace.findSealedPeriodForEpochScan(since); + } + return sealed; + } + + public static Sealed lookupForSnapshot(Epoch since) + { + Sealed sealed = index.get().lookupEpochForSnapshot(since); + if (sealed.equals(EMPTY)) + { + logger.info("No sealed period for snapshot found in index for epoch {}, querying system table", since); + sealed = SystemKeyspace.getLastSealedPeriod(); + } + return sealed; + } + + public static void recordSealedPeriod(long period, Epoch epoch) + { + SystemKeyspace.sealPeriod(period, epoch); + index.updateAndGet(i -> i.with(epoch, period)); + } + + public static void initIndexFromSystemTables() + { + int maxSize = CassandraRelevantProperties.TCM_RECENTLY_SEALED_PERIOD_INDEX_SIZE.getInt(); + List recentlySealed = new ArrayList<>(maxSize); + Sealed last = SystemKeyspace.getLastSealedPeriod(); + recentlySealed.add(last); + long previous = last.period - 1; + recentlySealed.addAll(Period.scanLogForRecentlySealed(SystemKeyspace.LocalMetadataLog, previous, maxSize - 1)); + index.set(RecentlySealedPeriods.init(recentlySealed)); + } + + @VisibleForTesting + public static void unsafeClearLookup() + { + index.set(RecentlySealedPeriods.EMPTY); + } + + public Sealed(long period, long epoch) + { + this(period, Epoch.create(epoch)); + } + + public Sealed(long period, Epoch epoch) + { + this.period = period; + this.epoch = epoch; + } + + @Override + public String toString() + { + return "Sealed{" + + "period=" + period + + ", epoch=" + epoch + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Sealed)) return false; + Sealed sealed = (Sealed) o; + return period == sealed.period && epoch.equals(sealed.epoch); + } + + @Override + public int hashCode() + { + return Objects.hash(period, epoch); + } + + @Override + public int compareTo(Sealed o) + { + return Long.compare(this.epoch.getEpoch(), o.epoch.getEpoch()); + } +} diff --git a/src/java/org/apache/cassandra/tcm/Startup.java b/src/java/org/apache/cassandra/tcm/Startup.java new file mode 100644 index 0000000000..c536bd3361 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Startup.java @@ -0,0 +1,500 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.BootStrapper; +import org.apache.cassandra.exceptions.StartupException; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.NewGossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.log.SystemKeyspaceStorage; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.migration.Election; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.sequences.ReplaceSameAddress; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.tcm.ClusterMetadataService.State.LOCAL; +import static org.apache.cassandra.tcm.compatibility.GossipHelper.emptyWithSchemaFromSystemTables; +import static org.apache.cassandra.tcm.compatibility.GossipHelper.fromEndpointStates; +import static org.apache.cassandra.tcm.membership.NodeState.JOINED; +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; + + /** + * Initialize + */ + public class Startup +{ + private static final Logger logger = LoggerFactory.getLogger(Startup.class); + + public static void initialize(Set seeds) throws InterruptedException, ExecutionException, IOException, StartupException + { + initialize(seeds, + p -> p, + () -> MessagingService.instance().waitUntilListeningUnchecked()); + } + + public static void initialize(Set seeds, + Function wrapProcessor, + Runnable initMessaging) throws InterruptedException, ExecutionException, IOException, StartupException + { + switch (StartupMode.get(seeds)) + { + case FIRST_CMS: + logger.info("Initializing as first CMS node in a new cluster"); + initializeAsNonCmsNode(wrapProcessor); + initializeAsFirstCMSNode(); + initMessaging.run(); + break; + case NORMAL: + logger.info("Initializing as non CMS node"); + initializeAsNonCmsNode(wrapProcessor); + initMessaging.run(); + break; + case VOTE: + logger.info("Initializing for discovery"); + initializeAsNonCmsNode(wrapProcessor); + initializeForDiscovery(initMessaging); + break; + case UPGRADE: + logger.info("Initializing from gossip"); + initializeFromGossip(wrapProcessor, initMessaging); + break; + case BOOT_WITH_CLUSTERMETADATA: + String fileName = CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA.getString(); + logger.warn("Initializing with cluster metadata from: {}", fileName); + reinitializeWithClusterMetadata(fileName, wrapProcessor, initMessaging); + break; + } + } + + /** + * Make this node a _first_ CMS node. + *

    + * (1) Append PreInitialize transformation to local in-memory log. When distributed metadata keyspace is initialized, a no-op transformation will + * be added to other nodes. This is required since as of now, no node actually owns distributed metadata keyspace. + * (2) Commit Initialize transformation, which holds a snapshot of metadata as of now. + *

    + * This process is applicable for gossip upgrades as well as regular vote-and-startup process. + */ + public static void initializeAsFirstCMSNode() + { + ClusterMetadataService.instance().log().bootstrap(FBUtilities.getBroadcastAddressAndPort()); + assert ClusterMetadataService.state() == LOCAL : String.format("Can't initialize as node hasn't transitioned to CMS state. State: %s.\n%s", ClusterMetadataService.state(), ClusterMetadata.current()); + Initialize initialize = new Initialize(ClusterMetadata.current()); + ClusterMetadataService.instance().commit(initialize); + } + + public static void initializeAsNonCmsNode(Function wrapProcessor) throws StartupException + { + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withStorage(LogStorage.SystemKeyspace) + .withDefaultListeners(); + ClusterMetadataService.setInstance(new ClusterMetadataService(new UniformRangePlacement(), + wrapProcessor, + ClusterMetadataService::state, + logSpec)); + ClusterMetadataService.instance().initRecentlySealedPeriodsIndex(); + ClusterMetadataService.instance().log().replayPersisted(); + ClusterMetadata replayed = ClusterMetadataService.instance().log().metadata(); + scrubDataDirectories(replayed); + replayed.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + ClusterMetadataService.instance().log().ready(); + + NodeId nodeId = ClusterMetadata.current().myNodeId(); + UUID currentHostId = SystemKeyspace.getLocalHostId(); + if (nodeId != null && !Objects.equals(nodeId.toUUID(), currentHostId)) + { + logger.info("NodeId is wrong, updating from {} to {}", currentHostId, nodeId.toUUID()); + SystemKeyspace.setLocalHostId(nodeId.toUUID()); + } + } + + public static void scrubDataDirectories(ClusterMetadata metadata) throws StartupException + { + // clean up debris in the rest of the keyspaces + for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces()) + { + // Skip system as we've already cleaned it + if (keyspace.name.equals(SchemaConstants.SYSTEM_KEYSPACE_NAME)) + continue; + + for (TableMetadata cfm : keyspace.tables) + { + ColumnFamilyStore.scrubDataDirectories(cfm); + } + } + } + + /** + * Initialization for Discovery. + * + * Node will attempt to discover other participants in the cluster by attempting to contact the seeds + * it is aware of. After discovery, the node with a smallest ip address will move to propose itself as + * a CMS initiator, and attempt to establish a CMS in via two-phase commit protocol. + */ + public static void initializeForDiscovery(Runnable initMessaging) + { + initMessaging.run(); + logger.debug("Discovering other nodes in the system"); + Discovery.DiscoveredNodes candidates = Discovery.instance.discover(); + if (candidates.kind() == Discovery.DiscoveredNodes.Kind.KNOWN_PEERS) + { + logger.debug("Got candidates: " + candidates); + Optional option = candidates.nodes().stream().min(InetAddressAndPort::compareTo); + InetAddressAndPort min; + if (!option.isPresent()) + { + if (DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort())) + min = FBUtilities.getBroadcastAddressAndPort(); + else + throw new IllegalArgumentException(String.format("Found no candidates during initialization. Check if the seeds are up: %s", DatabaseDescriptor.getSeeds())); + } + else + { + min = option.get(); + } + + // identify if you need to start the vote + if (min.equals(FBUtilities.getBroadcastAddressAndPort()) || FBUtilities.getBroadcastAddressAndPort().compareTo(min) < 0) + { + Election.instance.nominateSelf(candidates.nodes(), + Collections.singleton(FBUtilities.getBroadcastAddressAndPort()), + (cm) -> true, + null); + } + } + + while (!ClusterMetadata.current().epoch.isAfter(Epoch.FIRST)) + { + if (candidates.kind() == Discovery.DiscoveredNodes.Kind.CMS_ONLY) + { + RemoteProcessor.fetchLogAndWait(new RemoteProcessor.CandidateIterator(candidates.nodes(), false), + ClusterMetadataService.instance().log()); + } + else + { + Election.Initiator initiator = Election.instance.initiator(); + candidates = Discovery.instance.discoverOnce(initiator == null ? null : initiator.initiator); + } + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + + assert ClusterMetadata.current().epoch.isAfter(Epoch.FIRST); + Election.instance.migrated(); + } + + /** + * This should only be called during startup. + */ + public static void initializeFromGossip(Function wrapProcessor, Runnable initMessaging) throws StartupException + { + ClusterMetadata emptyFromSystemTables = emptyWithSchemaFromSystemTables(SystemKeyspace.allKnownDatacenters()); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(emptyFromSystemTables) + .withStorage(LogStorage.SystemKeyspace) + .withDefaultListeners(); + ClusterMetadataService.setInstance(new ClusterMetadataService(new UniformRangePlacement(), + wrapProcessor, + ClusterMetadataService::state, + logSpec)); + scrubDataDirectories(emptyFromSystemTables); + emptyFromSystemTables.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + ClusterMetadataService.instance().log().ready(); + initMessaging.run(); + try + { + CommitLog.instance.recoverSegmentsOnDisk(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + logger.debug("Starting to initialize ClusterMetadata from gossip"); + Map epStates = NewGossiper.instance.doShadowRound(); + logger.debug("Got epStates {}", epStates); + ClusterMetadata initial = fromEndpointStates(emptyFromSystemTables.schema, epStates); + logger.debug("Created initial ClusterMetadata {}", initial); + SystemKeyspace.setLocalHostId(initial.myNodeId().toUUID()); + ClusterMetadataService.instance().setFromGossip(initial); + Gossiper.instance.clearUnsafe(); + Gossiper.instance.maybeInitializeLocalState(SystemKeyspace.incrementAndGetGeneration()); + for (Map.Entry entry : initial.directory.states.entrySet()) + Gossiper.instance.mergeNodeToGossip(entry.getKey(), initial); + + // double check that everything was added, can remove once we are confident + ClusterMetadata cmGossip = fromEndpointStates(emptyFromSystemTables.schema, Gossiper.instance.getEndpointStates()); + assert cmGossip.equals(initial) : cmGossip + " != " + initial; + } + + public static void reinitializeWithClusterMetadata(String fileName, Function wrapProcessor, Runnable initMessaging) throws IOException + { + // First set a minimal ClusterMetadata as some deserialization depends + // on ClusterMetadata.current() to access the partitioner + StubClusterMetadataService initial = StubClusterMetadataService.forClientTools(); + ClusterMetadataService.unsetInstance(); + StubClusterMetadataService.setInstance(initial); + + ClusterMetadata metadata = ClusterMetadataService.deserializeClusterMetadata(fileName); + // if the partitioners are mismatching, we probably won't even get this far + if (metadata.partitioner != DatabaseDescriptor.getPartitioner()) + throw new IllegalStateException(String.format("When reinitializing with cluster metadata, the same " + + "partitioner must be used. Configured: %s, Serialized: %s", + DatabaseDescriptor.getPartitioner().getClass().getCanonicalName(), + metadata.partitioner.getClass().getCanonicalName())); + + if (!metadata.isCMSMember(FBUtilities.getBroadcastAddressAndPort())) + throw new IllegalStateException("When reinitializing with cluster metadata, we must be in the CMS"); + // can use local dc here since we know local host in the cms: + ClusterMetadata emptyFromSystemTables = emptyWithSchemaFromSystemTables(Collections.singleton(DatabaseDescriptor.getLocalDataCenter())); + metadata.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + metadata = metadata.forceEpoch(metadata.epoch.nextEpoch()); + ClusterMetadataService.unsetInstance(); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(metadata) + .withStorage(LogStorage.SystemKeyspace) + .withDefaultListeners() + .isReset(true) + .withReadyNotification(LocalLog.LogSpec.WhenReady.NONE); + + ClusterMetadataService.setInstance(new ClusterMetadataService(new UniformRangePlacement(), + wrapProcessor, + ClusterMetadataService::state, + logSpec)); + // When re-intializing from a loaded metadata instance we need to fire notifications using the delta between an + // empty metadata and the loaded one. So we configure the LogSpec not to do any notifications and handle it + // explicitly here. + ClusterMetadataService.instance().log().notifyListeners(emptyFromSystemTables); + ClusterMetadataService.instance().log().ready(); + initMessaging.run(); + ClusterMetadataService.instance().forceSnapshot(metadata.forceEpoch(metadata.nextEpoch())); + ClusterMetadataService.instance().sealPeriod(); + CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA.reset(); + assert ClusterMetadataService.state() == LOCAL; + assert ClusterMetadataService.instance() != initial : "Aborting startup as temporary metadata service is still active"; + } + + public static void startup(boolean finishJoiningRing, boolean shouldBootstrap, boolean isReplacing) + { + startup(() -> getInitialTransformation(finishJoiningRing, shouldBootstrap, isReplacing), finishJoiningRing, shouldBootstrap, isReplacing); + } + + /** + * Cassandra startup process: + * * assume that startup could have been interrupted at any point in time, and attempt to finish any in-process + * sequences + * * after finishing an existing in-progress sequence, if any, we should jump to JOINED + * * otherwise, we assume a fresh setup, in which case we grab a startup sequence (Join or Replace), and try to + * bootstrap + * + * @param initialTransformation supplier of the Transformation which initiates the startup sequence + * @param finishJoiningRing if false, node is left in a survey mode, which means it will receive writes in all cases + * except if it is replacing the node with same address + * @param shouldBootstrap if true, bootstrap streaming will be executed + * @param isReplacing true, if the node is replacing some other node (with same or different address). + */ + public static void startup(Supplier initialTransformation, boolean finishJoiningRing, boolean shouldBootstrap, boolean isReplacing) + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId self = metadata.myNodeId(); + + // finish in-progress sequences first + InProgressSequences.finishInProgressSequences(self); + metadata = ClusterMetadata.current(); + + switch (metadata.directory.peerState(self)) + { + case REGISTERED: + case LEFT: + if (isReplacing) + ReconfigureCMS.maybeReconfigureCMS(metadata, DatabaseDescriptor.getReplaceAddress()); + + ClusterMetadataService.instance().commit(initialTransformation.get()); + + InProgressSequences.finishInProgressSequences(self); + metadata = ClusterMetadata.current(); + + if (metadata.directory.peerState(self) == JOINED) + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + else + { + StorageService.instance.markBootstrapFailed(); + logger.info("Did not finish joining the ring; node state is {}, bootstrap state is {}", + metadata.directory.peerState(self), + SystemKeyspace.getBootstrapState()); + break; + } + case JOINED: + if (StorageService.isReplacingSameAddress()) + ReplaceSameAddress.streamData(self, metadata, shouldBootstrap, finishJoiningRing); + + // JOINED appears before BOOTSTRAPPING & BOOT_REPLACE so we can fall + // through when we start as REGISTERED/LEFT and complete a full startup + logger.info("{}", StorageService.Mode.NORMAL); + break; + case BOOTSTRAPPING: + case BOOT_REPLACING: + if (finishJoiningRing) + { + throw new IllegalStateException("Expected to complete startup sequence, but did not. " + + "Can't proceed from the state " + metadata.directory.peerState(self)); + } + break; + default: + throw new IllegalStateException("Can't proceed from the state " + metadata.directory.peerState(self)); + } + } + + /** + * Returns: + * * {@link PrepareReplace}, the first step of the multi-step replacement process sequence, if {@param finishJoiningRing} is true + * * {@link UnsafeJoin}, a single-step join transformation, if {@param shouldBootstrap} is set to false, and the node is not + * in a write survey mode (in other words {@param finishJoiningRing} is true. This mode is mostly used for testing, but can + * also be used to quickly set up a fresh cluster. + * * and {@link PrepareJoin}, the first step of the multi-step join process, otherwise. + */ + private static Transformation getInitialTransformation(boolean finishJoiningRing, boolean shouldBootstrap, boolean isReplacing) + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (isReplacing) + { + InetAddressAndPort replacingEndpoint = DatabaseDescriptor.getReplaceAddress(); + if (FailureDetector.instance.isAlive(replacingEndpoint)) + { + logger.error("Unable to replace live node {})", replacingEndpoint); + throw new UnsupportedOperationException("Cannot replace a live node... "); + } + + NodeId replaced = ClusterMetadata.current().directory.peerId(replacingEndpoint); + + return new PrepareReplace(replaced, + metadata.myNodeId(), + ClusterMetadataService.instance().placementProvider(), + finishJoiningRing, + shouldBootstrap); + } + else if (finishJoiningRing && !shouldBootstrap) + { + return new UnsafeJoin(metadata.myNodeId(), + new HashSet<>(BootStrapper.getBootstrapTokens(ClusterMetadata.current(), getBroadcastAddressAndPort())), + ClusterMetadataService.instance().placementProvider()); + } + else + { + return new PrepareJoin(metadata.myNodeId(), + new HashSet<>(BootStrapper.getBootstrapTokens(ClusterMetadata.current(), getBroadcastAddressAndPort())), + ClusterMetadataService.instance().placementProvider(), + finishJoiningRing, + shouldBootstrap); + } + } + + /** + * Initialization process + */ + + enum StartupMode + { + /** + * Normal startup mode: node has already been a part of a CMS, and was restarted. + */ + NORMAL, + /** + * An upgrade path from Gossip: node has been booted brefore and was a part of a Gossip cluster. + */ + UPGRADE, + /** + * Node is starting for the first time, and should attempt to either discover existing CMS, or + * participate in the leader election to establish a new one. + */ + VOTE, + /** + * Node is starting for the first time, and is a designated first CMS node and can become a first CMS + * node upon boot. + */ + FIRST_CMS, + /** + * Node has to pick Cluster Metadata from the specified file. Used for testing and for (improbable) disaster recovery. + */ + BOOT_WITH_CLUSTERMETADATA; + + static StartupMode get(Set seeds) + { + if (CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA.isPresent()) + { + logger.warn("Booting with ClusterMetadata from file: " + CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA.getString()); + return BOOT_WITH_CLUSTERMETADATA; + } + if (seeds.isEmpty()) + throw new IllegalArgumentException("Can not initialize CMS without any seeds"); + + boolean hasAnyEpoch = SystemKeyspaceStorage.hasAnyEpoch(); + // For CCM and local dev clusters + boolean isOnlySeed = DatabaseDescriptor.getSeeds().size() == 1 + && DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddressAndPort()) + && DatabaseDescriptor.getSeeds().iterator().next().getAddress().isLoopbackAddress(); + boolean hasBootedBefore = SystemKeyspace.getLocalHostId() != null; + logger.info("hasAnyEpoch = {}, hasBootedBefore = {}", hasAnyEpoch, hasBootedBefore); + if (!hasAnyEpoch && hasBootedBefore) + return UPGRADE; + else if (hasAnyEpoch) + return NORMAL; + else if (isOnlySeed) + return FIRST_CMS; + else + return VOTE; + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/StubClusterMetadataService.java b/src/java/org/apache/cassandra/tcm/StubClusterMetadataService.java new file mode 100644 index 0000000000..066b33ceb0 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/StubClusterMetadataService.java @@ -0,0 +1,124 @@ +/* + * 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.tcm; + +import java.util.Collections; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; + +public class StubClusterMetadataService extends ClusterMetadataService +{ + + public static StubClusterMetadataService forClientTools() + { + DatabaseDescriptor.setLocalDataCenter("DC1"); + KeyspaceMetadata ks = DistributedMetadataLogKeyspace.initialMetadata(Collections.singleton("DC1")); + DistributedSchema schema = new DistributedSchema(Keyspaces.of(ks)); + return new StubClusterMetadataService(new ClusterMetadata(DatabaseDescriptor.getPartitioner(), + Directory.EMPTY, + schema)); + } + + public static StubClusterMetadataService forClientTools(DistributedSchema initialSchema) + { + DatabaseDescriptor.setLocalDataCenter("DC1"); + ClusterMetadata metadata = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + metadata = metadata.transformer().with(initialSchema).build().metadata.forceEpoch(Epoch.EMPTY); + return new StubClusterMetadataService(metadata); + } + + public static StubClusterMetadataService forTesting() + { + return new StubClusterMetadataService(new ClusterMetadata(DatabaseDescriptor.getPartitioner())); + } + + public static StubClusterMetadataService forTesting(ClusterMetadata metadata) + { + return new StubClusterMetadataService(metadata); + } + + private ClusterMetadata metadata; + + private StubClusterMetadataService(ClusterMetadata initial) + { + super(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + LocalLog.sync(new LocalLog.LogSpec().withInitialState(initial)), + new StubProcessor(), + Commit.Replicator.NO_OP, + false); + this.metadata = initial; + this.log().ready(); + } + + @Override + public T1 commit(Transformation transform, CommitSuccessHandler onSuccess, CommitFailureHandler onFailure) + { + Transformation.Result result = transform.execute(metadata); + if (result.isSuccess()) + { + metadata = result.success().metadata; + return onSuccess.accept(result.success().metadata); + } + return onFailure.accept(result.rejected().code, result.rejected().reason); + } + + @Override + public ClusterMetadata fetchLogFromCMS(Epoch awaitAtLeast) + { + return metadata; + } + + @Override + public ClusterMetadata metadata() + { + return metadata; + } + + public void setMetadata(ClusterMetadata metadata) + { + this.metadata = metadata; + } + + private static class StubProcessor implements Processor + { + + private StubProcessor() {} + + @Override + public Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy) + { + throw new UnsupportedOperationException(); + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/TCM_implementation.md b/src/java/org/apache/cassandra/tcm/TCM_implementation.md new file mode 100644 index 0000000000..8fc0da5bbd --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/TCM_implementation.md @@ -0,0 +1,75 @@ + + +# TCM Implementation Details + +This document will walk you through the core classes involved in Transactional Cluster Metadata. It describes a process of a node bringup into the existing TCM cluster. Each section will be prefixed by the header holding key classes that are used/described in the setion. + +## Startup: Discovery, Vote + +Boot process in TCM is very similar to the previously existing one, but is now split into several different classes rather than being mostly in `StorageService`. At first, `ClusterMetadataService` is initialized using `Startup#initialize`. Node determines its startup mode, which will be `Vote` in a usual case, which means that the node will initialize itself as a non-CMS node and will attempt to discover an existing CMS service or, failing that, participate in a vote to establish a new one with other discovered participants. If the seeds are configured correctly, the node is going to learn from the seed about existing CMS nodes, and will try contacting them to fetch the initial log. + +Node then continues startup, and eventually gets to `StorageService#initServer`, where it, among other things, gossips with CMS nodes to get a fresh view of the cluster for FD purposes, and then waits for Gossip to settle (again, for FD purposes). + +## Registration: ClusterMetadata, Transformation, RemoteProcessor + +Before joining the ring, the node has to register in order to obtain `NodeId`, which happens in `Register#maybeRegister`. Registration happens by committing a `Register` transformation using `ClusterMetadataService#commit` method. `Register` and other transformations are side-effect free functions mapping an instance of immutable `ClusterMetadata` to next `ClusterMetadata`. `ClusterMetadata` holds all information about cluster: directory of registered nodes, schema, node states and data ownership. + +Since the node executing register is not a CMS node, it is going to use a `RemoteProcessor` in order to perform this commit. `RemoteProcessor` is a simple RPC tool that serializes transformation and attempts to execute it by contacting CMS nodes and sending them `TCM_COMMIT_REQ`. + +## Commit Request: PaxosBackedProcessor, DistributedMetadataLogKeyspace, Retry + +When a CMS node receives a commit request, it deserializes and attempts to execute the transformation using `PaxosBackedProcessor`. Paxos backed processor stores an entire cluster metadata log in the `system_cluster_metadata.``distributed_metadata_log` table. It performs a simple CAS LWT that attempts to append a new entry to the log with an `Epoch` that is strictly consecutive to the last one. `Epoch` is a monotonically incrementing counter of `ClusterMetadata` versions. + +Both remote and paxos-backed processors are using `Retry` class for managing retries. Remote processor sets a deadline for its retries using `tcm_await_timeout`. CMS-local processor permits itself to use at most `tcm_rpc_timeout` for its attempts to retry. + +`PaxosBackedProcessor` then attempts to execute `Transformation`. Result of the execution can be either `Success` or `Reject`. `Reject`s are not persisted in the log, and are linearized using a read that confirms that transformation was executed against the highest epoch. Examples of `Reject`s are validation errors, exceptions encountered while attempting to execute transformation, etc. For example, `Register` would return a rejection if a node with the same IP address already exists in the registry. + +## Commit Response: Entry, LocalLog + +After `PaxosBackedProcessor` suceeds with committing the entry to the distributed log, it broadcasts the commit result that contains `Entry` holding newly appended transformation to the rest of the cluster using `Replicator` (which simply iterates all nodes in the directory, informing them about the new epoch). This operation does not need to be reliable and has no retries. In other words, if a node was down during CMS attempt to replicate entries to it, it will inevitably learn about the new epoch later when it comes back alive. + +Along with committed `Entry`, the response from CMS to the peer which submitted it also contains all entries that will allow the node that has initiated the commit to fully catch up to the epoch enacted by the committed transformation. + +When `RemoteProcessor` receives a response from CMS node, it appends all received entries to the `LocalLog`. `LocalLog` processes the backlog of pending entries and enacts a new epoch by constructing new `ClusterMetadata`. + +## Bootstrap: InProgressSequence, PrepareJoin, BootstrapAndJoin + +At that point, the node is ready to start the process of joining the ring. It begins in `Startup#startup`. `Startup#getInitialTransformation` determines that the node should start regular bootstrap process (as opposed to replace), and the node proceeds with commit of `PrepareJoin` transformation. During `PrepareJoin`, `ClusterMetadata` is changed in the following ways: + +* Ranges that will be affected by the bootstrap of the node are locked (see `LockedRanges`) + * If computed locked ranges intersect with ranges that were locked before this transformation got executed, `PrepareJoin` is rejected. +* `InProgressSequence`, holding the three transformations (`PrepareJoin`, `MinJoin` `FinishJoin`), is computed and added to `InProgressSequences` map. + * If any in-progress sequences associated with the current node are present, `PrepareJoin` is rejected. +* `AffectedRanges`, ranges whose placements are going to be changed while executing this sequence, are computed and returned as a part of commit success message. + +`InProgressSequence` is then executed step-by step. All local operations that the node has to perform between executing these steps are implemented as a part of the in-progress sequence (see `BootstrapAndJoin#executeNext`). We make *no assumptions* about liveness of the node between execution of in-progress sequence steps. For example, the node may crash after executing `PrepareJoin` but before it updates tokens in the local keyspace. So the only assumption we make is that `SystemKeyspace.updateLocalTokens` has to be called *before* `StartJoin` is committed. Similarly, owned data has to be streamed towards the node *before* it becomes a part of a read quorum, so even if the node crashes or is restarted an arbitrary number of times during streaming. + +In order to ensure quorum consistency, before executing each next step, the node has to await on the `ProgressBarrier`. CEP-21 contains a detailed explanation about why progress barriers are necessary. For the purpose of this document, it suffices to say that majority of owners of the `AffectedRanges` have to learn about the epoch enacting the previous step before each next step can be executed. This is done in order to preserve replication factor for eventually consistent queries. + +Upon executing all steps in the progress sequence, ranges are unlocked, and sequence itself is removed from `ClusterMetadata`. + + +## Querying: CoordinatorBehindException, FetchPeerLog, FetchCMSLog + +As the node starts participating in reads and writes, it may happen that its view of the ring or schema becomes divergent from other nodes. TCM makes best effort to minimize the time window of this happening, but in a distributed system at least some delay is inevitable. TCM solves this problem by including the highest `Epoch` known by the node in every request that the node coordinates, and in every response to the coordinator when serving as a replica. + +Replicas can check the schema and ring consistency of the *current* request by comparing the `Epoch` that coordinator has with the epoch when schema was last modified, and when the placements for the given range were last modified. If it happens that the replica knows that coordinator couldn’t have known about either schema, or the ring, it will throw `CoordinatorBehindException`. In all other cases (i.e. when either coordinator, or the replica are aware of the higher `Epoch`, but existence of this epoch does not influence consistency or outcome of the given query), lagging participant will issue an asynchonous `TCM_FETCH_PEER_LOG_REQ` and attempt to catch up from the peer. Failing that, it will attempt to catch up from the CMS node using `TCM_FETCH_CMS_LOG_REQ`. + +After coordinator has collected enough responses, it compares its `Epoch` with the `Epoch` that was used to construct the `ReplicaPlan` for the query it is coordinating. If epochs are different, it checks if collected replica responses still correspond to the consistency level query was executed at. + diff --git a/src/java/org/apache/cassandra/tcm/TransactionalClusterMetadata.md b/src/java/org/apache/cassandra/tcm/TransactionalClusterMetadata.md new file mode 100644 index 0000000000..75fada86ae --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/TransactionalClusterMetadata.md @@ -0,0 +1,77 @@ + + +# Transactional Cluster Metadata (TCM) + +### More Detail + +Further detail on the motivation, design, constraints and guarantees of TCM can be found in the Cassandra Enhancement Proposal document at https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-21%3A+Transactional+Cluster+Metadata + +Lower level implementation details regarding certain areas of the codebase can be found in-tree in TCM_implementation.md + +## ClusterMetadata + +`ClusterMetadata` is the immutable data object representing metadata about the state of the cluster. + +## Cluster Metadata Service +A subset of nodes in the cluster act as members of the Cluster Metadata Service (CMS) to maintain a log of events which modify cluster wide metadata. Membership of the CMS is flexible and dynamic and is updated when any of the existing members are decommissioned or replaced. + +Members of the CMS are responsible for linearizing insertion of entries into the metadata change log. An entry contains a transformation to be applied to ClusterMetadata and a monotonically increasing counter, or epoch. Each event committed to the log by the CMS implies a new epoch and as such, an epoch represents a specific point in the linearized history of cluster metadata. + + Clients of the CMS submit metadata change events and the CMS node receiving it determines whether that event should be executed or rejected based on the current metadata state. The CMS enforces a serial order (initially using Paxos LWTs to insert into a distributed log table) for accepted events and proactively disseminates them to the rest of the cluster. + +Independently, each node in the cluster applies the replicated transformations in strict order to produce a consistent view of metadata. After doing so, the transformed `ClusterMetadata` is atomically published on the node so that the most current metadata is always available. Code consuming metadata never witnesses partial updates to `ClusterMetadata`, and each version is uniquely identifiable by its epoch. + +## Schema + +`DistributedSchema` is a component of `ClusterMetadata` and all DDL updates are applied by the CMS inserting a log entry containing a schema transformation. As the log entries are disseminated around the cluster, each peer applies the transformation to its local `ClusterMetadata`, enacting the schema change. This entails some changes to the way the database objects represented in schema are intialised locally (db objects refers to classes like `Keyspace`, `ColumnFamilyStore`, etc). + +## Cluster Membership + +The `Directory` component of `ClusterMetadata` manages member identity, state, location and addressing. This duplicates many of the functions previously performed by `TokenMetadata`, `Topology` et al but with updates performed in deterministic order via the global log. + +## Data Placement + +`DataPlacements` provide a mapping from (keyspace, token range) tuples to sets of replicas. There are several such mappings in a cluster, one for each distinct set of `ReplicationParams` belonging to keyspaces in the schema. Where previously these mappings would be calculated on demand as topology modifications were discovered via gossip, these are now statically constructed for/from a given `ClusterMetadata` version. + +## Operations + +Performing operations such as shrink, expand, node replacement etc, do not simply modify metadata so atomic updates to cluster metadata are not in themselves sufficient for performing these safely. Streaming transfer of data between nodes must also occur and this must happen concurrently with reads and writes. Replication factors for writes must be temporarily expanded during range movements, what was previously provided by the concept of `PendingRanges`. + +With TCM, these operations are performed in coordinated phases, planned in advance and properly linearized using the global log. For instance, to join an new node, the full set of phased range movements required is calculated to generate an actionable plan which is then stored in `InProgressSequences`, another component of `ClusterMetadata`. + +Concurrent operations are permitted as long as they only affect disjoint token ranges, ensuring that concurrent range movements remain safe and cluster invariants are preserved at all times, including in the case of the failure to complete any operation (i.e. failed or aborted bootstrap). + +## Read/Write Consistency + +During a read or write, the coordinator constructs a replica plan based on the cluster metadata that is known to it at the time. The `Mutation` or`ReadCommand` messages that the coordinator sends to those replicas include the epochs which identify the most recent changes to either `DataPlacements` or `TableMetadata` which may have a material effect on the execution of the message payload. This allows replicas and coordinators to detect divergence between their respective metadata and take action accordingly. Coordinators also re-check `DataPlacements` after collecting replica responses, before returning to the client. If these have changed while the request was in flight, the coordinator can then verify whether received responses meet the desired consistency given the new topology. + +## Recovering Missing Log Entries + +A node may detect that it is missing some entries from the metadata log, either by receiving an update from the CMS which is non-consecutive with its current metadata epoch or during an exchange of messages with a peer. Catching up can be done simply by requesting the sequence of log entries with a greater epoch than any previously seen by the node. As the log is immutable and totally ordered, this request can be made to any peer as the results must be consistent, but not exhaustive. Any peers themselves may be lagging behind the "true" tail of the log, but this is perfectly acceptable, as it is impossible to propagate changes to all participants simultaneously, and we achieve correctness by means other than synchrony. + +Nodes provide each other with missing log entries in the form of a `LogState`, which comprises an optional snapshot of `ClusterMetadata` at a given epoch, plus a list of individual log entries (a `Replication` object) to apply on top of the snapshot (if present). When receiving a `LogState`, the node can use the snapshot to effectively skip ahead to a precise point in the linearized history without requiring every intermediate transformation individually. To do this, it constructs a synthetic log entry containing a `ForceSnapshot` transformation which it inserts at the head of its local buffer of pending log entries. The log consumer then applies this transformation, producing a new `ClusterMetadata` (the exact one contained in the `LogState`) and publishing it as it would the result of any other transformation. + +## Upgrading and Transitioning to TCM +Following an upgrade, nodes in an existing cluster will enter a minimal modification mode. In this state, the set of allowed cluster metadata modifications is constrained to include only the addition, removal and replacement of nodes, to allow failed hosts to be replaced during the +upgrade. In this mode the CMS has no members and each peer maintains its own `ClusterMetadata` instance independently. This metadata is intitialised at startup from system tables and gossip is used to propagate the permitted subset of metadata changes. + +When the operator is ready, one node is chosen for promotion to the initial CMS, which is done manually via nodetool `initializecms`. At this point, the candidate node will propose itself as the initial CMS and attempt to gain consensus from the rest of the cluster. If successful, it verifies that all peers have an identical view of cluster metadata and initialises the distributed log with a snapshot of that metadata. + +Once this process is complete all future cluster metadata updates are performed via the CMS using the global log and reverting to the previous method of metadata management is not supported. Further members can and should be added to the CMS using nodetool's `reconfigurecms` command. + diff --git a/src/java/org/apache/cassandra/tcm/Transformation.java b/src/java/org/apache/cassandra/tcm/Transformation.java new file mode 100644 index 0000000000..df15598b4d --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/Transformation.java @@ -0,0 +1,320 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Ints; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.ExceptionCode; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.sequences.CancelCMSReconfiguration; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.*; +import org.apache.cassandra.tcm.transformations.Startup; +import org.apache.cassandra.tcm.transformations.cms.AdvanceCMSReconfiguration; +import org.apache.cassandra.tcm.transformations.cms.FinishAddToCMS; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.tcm.transformations.cms.PreInitialize; +import org.apache.cassandra.tcm.transformations.cms.RemoveFromCMS; +import org.apache.cassandra.tcm.transformations.cms.StartAddToCMS; +import org.apache.cassandra.tcm.transformations.cms.PrepareCMSReconfiguration; + +public interface Transformation +{ + Serializer transformationSerializer = new Serializer(); + + Kind kind(); + + /** + * Performs the core function of the transformation, to transition ClusterMetadata from one state to the next. + * Returns a {@link Result}, either {@link Success} or {@link Rejected}. The former contains the transformed + * metadata along with an indication of which specific components were modified. It also includes the set of token + * ranges impacted by the change (if any). A {@link Rejected} result contains an {@link ExceptionCode} and reason string. + * @param metadata the starting state + * @return a result object indicating a success or failure in applying transformation + */ + Result execute(ClusterMetadata metadata); + + default boolean allowDuringUpgrades() + { + return false; + } + + static Success success(ClusterMetadata.Transformer transformer, LockedRanges.AffectedRanges affectedRanges) + { + ClusterMetadata.Transformer.Transformed transformed = transformer.build(); + return new Success(transformed.metadata, affectedRanges, transformed.modifiedKeys); + } + + interface Result + { + boolean isSuccess(); + boolean isRejected(); + + Success success(); + Rejected rejected(); + } + + final class Success implements Result + { + public final ClusterMetadata metadata; + public final LockedRanges.AffectedRanges affectedRanges; + public final ImmutableSet affectedMetadata; + + public Success(ClusterMetadata metadata, LockedRanges.AffectedRanges affectedRanges, ImmutableSet affectedKeys) + { + this.metadata = metadata; + this.affectedRanges = affectedRanges; + this.affectedMetadata = affectedKeys; + } + + public boolean isSuccess() + { + return true; + } + + public boolean isRejected() + { + return false; + } + + public Success success() + { + return this; + } + + public Rejected rejected() + { + throw new IllegalStateException("Can't dereference Rejection upon successful execution"); + } + + public String toString() + { + return "Result{" + + "newState=" + metadata + + '}'; + } + } + + final class Rejected implements Result + { + public final ExceptionCode code; + public final String reason; + + public Rejected(ExceptionCode code, String reason) + { + this.code = code; + this.reason = reason; + } + + public boolean isSuccess() + { + return false; + } + + public boolean isRejected() + { + return true; + } + + public Success success() + { + throw new IllegalStateException("Can't dereference Success for a rejected execution"); + } + + public Rejected rejected() + { + return this; + } + + public String toString() + { + return "Rejected{" + + "code=" + code + + ", reason='" + reason + '\'' + + '}'; + } + } + + enum Kind + { + PRE_INITIALIZE_CMS(() -> PreInitialize.serializer), + INITIALIZE_CMS(() -> Initialize.serializer), + FORCE_SNAPSHOT(() -> ForceSnapshot.serializer), + SEAL_PERIOD(() -> SealPeriod.serializer), + SCHEMA_CHANGE(() -> AlterSchema.serializer), + REGISTER(() -> Register.serializer), + UNREGISTER(() -> Unregister.serializer), + + UNSAFE_JOIN(() -> UnsafeJoin.serializer), + PREPARE_JOIN(() -> PrepareJoin.serializer), + START_JOIN(() -> PrepareJoin.StartJoin.serializer), + MID_JOIN(() -> PrepareJoin.MidJoin.serializer), + FINISH_JOIN(() -> PrepareJoin.FinishJoin.serializer), + + PREPARE_MOVE(() -> PrepareMove.serializer), + START_MOVE(() -> PrepareMove.StartMove.serializer), + MID_MOVE(() -> PrepareMove.MidMove.serializer), + FINISH_MOVE(() -> PrepareMove.FinishMove.serializer), + + PREPARE_LEAVE(() -> PrepareLeave.serializer), + START_LEAVE(() -> PrepareLeave.StartLeave.serializer), + MID_LEAVE(() -> PrepareLeave.MidLeave.serializer), + FINISH_LEAVE(() -> PrepareLeave.FinishLeave.serializer), + ASSASSINATE(() -> Assassinate.serializer), + + PREPARE_REPLACE(() -> PrepareReplace.serializer), + START_REPLACE(() -> PrepareReplace.StartReplace.serializer), + MID_REPLACE(() -> PrepareReplace.MidReplace.serializer), + FINISH_REPLACE(() -> PrepareReplace.FinishReplace.serializer), + + CANCEL_SEQUENCE(() -> CancelInProgressSequence.serializer), + + @Deprecated(since = "CEP-21") + START_ADD_TO_CMS(() -> StartAddToCMS.serializer), + @Deprecated(since = "CEP-21") + FINISH_ADD_TO_CMS(() -> FinishAddToCMS.serializer), + @Deprecated(since = "CEP-21") + REMOVE_FROM_CMS(() -> RemoveFromCMS.serializer), + + STARTUP(() -> Startup.serializer), + + CUSTOM(() -> CustomTransformation.serializer), + + PREPARE_SIMPLE_CMS_RECONFIGURATION(() -> PrepareCMSReconfiguration.Simple.serializer), + PREPARE_COMPLEX_CMS_RECONFIGURATION(() -> PrepareCMSReconfiguration.Complex.serializer), + ADVANCE_CMS_RECONFIGURATION(() -> AdvanceCMSReconfiguration.serializer), + CANCEL_CMS_RECONFIGURATION(() -> CancelCMSReconfiguration.serializer) + ; + + private final Supplier> serializer; + + Kind(Supplier> serializer) + { + this.serializer = serializer; + } + + public AsymmetricMetadataSerializer serializer() + { + return serializer.get(); + } + + public ByteBuffer toVersionedBytes(Transformation transform) throws IOException + { + Version serializationVersion = Version.minCommonSerializationVersion(); + long size = VerboseMetadataSerializer.serializedSize(serializer.get(), transform, serializationVersion); + ByteBuffer bb = ByteBuffer.allocate(Ints.checkedCast(size)); + try (DataOutputBuffer out = new DataOutputBuffer(bb)) + { + VerboseMetadataSerializer.serialize(serializer.get(), transform, out, serializationVersion); + } + bb.flip(); + return bb; + } + + public Transformation fromVersionedBytes(ByteBuffer bb) throws IOException + { + try (DataInputBuffer in = new DataInputBuffer(bb, true)) + { + return VerboseMetadataSerializer.deserialize(serializer.get(), in); + } + } + } + + class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + out.writeUnsignedVInt32(t.kind().ordinal()); + t.kind().serializer().serialize(t, out, version); + } + + public Transformation deserialize(DataInputPlus in, Version version) throws IOException + { + Kind kind = Kind.values()[in.readUnsignedVInt32()]; + return kind.serializer().deserialize(in, version); + } + + public long serializedSize(Transformation t, Version version) + { + return TypeSizes.sizeofUnsignedVInt(t.kind().ordinal()) + + t.kind().serializer().serializedSize(t, version); + } + } + + /** + * Used on a CMS node that attempts to commit the transformation to avoid + * calling `execute` twice. + */ + final class Executed implements Transformation + { + private final Transformation delegate; + private final Result result; + + public Executed(Transformation delegate, Result result) + { + this.delegate = delegate; + this.result = result; + } + + public Kind kind() + { + return delegate.kind(); + } + + public Result execute(ClusterMetadata clusterMetadata) + { + return result; + } + + public Transformation original() + { + return delegate; + } + + @Override + public String toString() + { + return "EXECUTED {" + delegate.toString() + "}"; + } + } + + /** + * Allowed to be thrown inside transformations to signal that the error is expected by the transformation, and + * does not constitute a reason for retry. + */ + class RejectedTransformationException extends RuntimeException + { + public RejectedTransformationException(String message) + { + super(message); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/compatibility/GossipHelper.java b/src/java/org/apache/cassandra/tcm/compatibility/GossipHelper.java new file mode 100644 index 0000000000..ab4ce55f11 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/compatibility/GossipHelper.java @@ -0,0 +1,392 @@ +/* + * 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.tcm.compatibility; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.HeartBeatState; +import org.apache.cassandra.gms.TokenSerializer; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.SchemaKeyspace; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.extensions.ExtensionKey; +import org.apache.cassandra.tcm.extensions.ExtensionValue; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.TokenMap; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.Move; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.gms.ApplicationState.DC; +import static org.apache.cassandra.gms.ApplicationState.HOST_ID; +import static org.apache.cassandra.gms.ApplicationState.INTERNAL_ADDRESS_AND_PORT; +import static org.apache.cassandra.gms.ApplicationState.INTERNAL_IP; +import static org.apache.cassandra.gms.ApplicationState.NATIVE_ADDRESS_AND_PORT; +import static org.apache.cassandra.gms.ApplicationState.RACK; +import static org.apache.cassandra.gms.ApplicationState.RELEASE_VERSION; +import static org.apache.cassandra.gms.ApplicationState.RPC_ADDRESS; +import static org.apache.cassandra.gms.ApplicationState.STATUS_WITH_PORT; +import static org.apache.cassandra.gms.ApplicationState.TOKENS; +import static org.apache.cassandra.gms.Gossiper.isShutdown; +import static org.apache.cassandra.locator.InetAddressAndPort.getByName; +import static org.apache.cassandra.locator.InetAddressAndPort.getByNameOverrideDefaults; +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; + +public class GossipHelper +{ + private static final Logger logger = LoggerFactory.getLogger(GossipHelper.class); + + public static void removeFromGossip(InetAddressAndPort addr) + { + Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.removeEndpoint(addr)); + } + + public static void evictFromMembership(InetAddressAndPort endpoint) + { + Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.evictFromMembership(endpoint)); + } + + public static VersionedValue nodeStateToStatus(NodeId nodeId, + ClusterMetadata metadata, + Collection tokens, + VersionedValue.VersionedValueFactory valueFactory, + VersionedValue oldValue) + { + NodeState nodeState = metadata.directory.peerState(nodeId); + if ((tokens == null || tokens.isEmpty()) && !NodeState.isBootstrap(nodeState)) + return null; + + MultiStepOperation sequence; + VersionedValue status = null; + switch (nodeState) + { + case JOINED: + if (isShutdown(oldValue)) + status = valueFactory.shutdown(true); + else + status = valueFactory.normal(tokens); + break; + case LEFT: + status = valueFactory.left(tokens, Gossiper.computeExpireTime()); + break; + case BOOTSTRAPPING: + sequence = metadata.inProgressSequences.get(nodeId); + if (!(sequence instanceof BootstrapAndJoin)) + { + logger.error(String.format("Cannot construct gossip state. Node is in %s state, but the sequence is %s", NodeState.BOOTSTRAPPING, sequence)); + return null; + } + Collection bootstrapTokens = getTokensFromOperation(sequence); + status = valueFactory.bootstrapping(bootstrapTokens); + break; + case BOOT_REPLACING: + sequence = metadata.inProgressSequences.get(nodeId); + if (!(sequence instanceof BootstrapAndReplace)) + { + logger.error(String.format("Cannot construct gossip state. Node is in %s state, but the sequence is %s", NodeState.BOOT_REPLACING, sequence)); + return null; + } + + NodeId replaced = ((BootstrapAndReplace)sequence).startReplace.replaced(); + if (metadata.directory.versions.values().stream().allMatch(NodeVersion::isUpgraded)) + status = valueFactory.bootReplacingWithPort(metadata.directory.endpoint(replaced)); + else + status = valueFactory.bootReplacing(metadata.directory.endpoint(replaced).getAddress()); + break; + case LEAVING: + status = valueFactory.leaving(tokens); + break; + case MOVING: + sequence = metadata.inProgressSequences.get(nodeId); + if (!(sequence instanceof Move)) + { + logger.error(String.format("Cannot construct gossip state. Node is in %s state, but sequence the is %s", NodeState.MOVING, sequence)); + return null; + } + Collection moveTokens = getTokensFromOperation(sequence); + if (!moveTokens.isEmpty()) + { + Token token = ((Move) sequence).tokens.iterator().next(); + status = valueFactory.moving(token); + } + break; + case REGISTERED: + break; + default: + throw new RuntimeException("Bad NodeState " + nodeState); + } + return status; + } + + public static Collection getTokensFromOperation(NodeId nodeId, ClusterMetadata metadata) + { + return getTokensFromOperation(metadata.inProgressSequences.get(nodeId)); + } + + public static Collection getTokensFromOperation(MultiStepOperation sequence) + { + if (null == sequence) + return Collections.emptySet(); + + if (sequence.kind() == MultiStepOperation.Kind.JOIN) + return new HashSet<>(((BootstrapAndJoin)sequence).finishJoin.tokens); + else if (sequence.kind() == MultiStepOperation.Kind.REPLACE) + return new HashSet<>(((BootstrapAndReplace)sequence).bootstrapTokens); + else if (sequence.kind() == MultiStepOperation.Kind.MOVE) + return new HashSet<>(((Move)sequence).tokens); + + throw new IllegalArgumentException(String.format("Extracting tokens from %s sequence is " + + "neither necessary nor supported here")); + } + + private static Collection getTokensIn(IPartitioner partitioner, EndpointState epState) + { + try + { + if (epState == null) + return Collections.emptyList(); + + VersionedValue versionedValue = epState.getApplicationState(TOKENS); + if (versionedValue == null) + return Collections.emptyList(); + + return TokenSerializer.deserialize(partitioner, new DataInputStream(new ByteArrayInputStream(versionedValue.toBytes()))); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + private static NodeState toNodeState(InetAddressAndPort endpoint, EndpointState epState) + { + assert epState != null; + + String status = epState.getStatus(); + if (status.equals(VersionedValue.STATUS_NORMAL) || + status.equals(VersionedValue.SHUTDOWN)) + return NodeState.JOINED; + if (status.equals(VersionedValue.STATUS_LEFT)) + return NodeState.LEFT; + throw new IllegalStateException("Can't upgrade the first node when STATUS = " + status + " for node " + endpoint); + } + + private static NodeAddresses getAddressesFromEndpointState(InetAddressAndPort endpoint, EndpointState epState) + { + if (endpoint.equals(getBroadcastAddressAndPort())) + return NodeAddresses.current(); + try + { + InetAddressAndPort local = getEitherState(endpoint, epState, INTERNAL_ADDRESS_AND_PORT, INTERNAL_IP, DatabaseDescriptor.getStoragePort()); + InetAddressAndPort nativeAddress = getEitherState(endpoint, epState, NATIVE_ADDRESS_AND_PORT, RPC_ADDRESS, DatabaseDescriptor.getNativeTransportPort()); + return new NodeAddresses(UUID.randomUUID(), endpoint, local, nativeAddress); + } + catch (UnknownHostException e) + { + throw new ConfigurationException("Unknown host in epState for " + endpoint + " : " + epState, e); + } + } + + private static InetAddressAndPort getEitherState(InetAddressAndPort endpoint, + EndpointState epState, + ApplicationState primaryState, + ApplicationState deprecatedState, + int defaultPortForDeprecatedState) throws UnknownHostException + { + if (epState.getApplicationState(primaryState) != null) + { + return getByName(epState.getApplicationState(primaryState).value); + } + else if (epState.getApplicationState(deprecatedState) != null) + { + return getByNameOverrideDefaults(epState.getApplicationState(deprecatedState).value, defaultPortForDeprecatedState); + } + else + { + return endpoint.withPort(defaultPortForDeprecatedState); + } + } + + private static NodeVersion getVersionFromEndpointState(InetAddressAndPort endpoint, EndpointState epState) + { + if (endpoint.equals(getBroadcastAddressAndPort())) + return NodeVersion.CURRENT; + CassandraVersion cassandraVersion = epState.getReleaseVersion(); + return NodeVersion.fromCassandraVersion(cassandraVersion); + } + + public static ClusterMetadata emptyWithSchemaFromSystemTables(Set allKnownDatacenters) + { + return new ClusterMetadata(Epoch.UPGRADE_STARTUP, + Period.EMPTY, + true, + DatabaseDescriptor.getPartitioner(), + DistributedSchema.fromSystemTables(SchemaKeyspace.fetchNonSystemKeyspaces(), allKnownDatacenters), + Directory.EMPTY, + new TokenMap(DatabaseDescriptor.getPartitioner()), + DataPlacements.empty(), + LockedRanges.EMPTY, + InProgressSequences.EMPTY, + Collections.emptyMap()); + } + + public static ClusterMetadata fromEndpointStates(DistributedSchema schema, Map epStates) + { + return fromEndpointStates(epStates, DatabaseDescriptor.getPartitioner(), schema); + } + + /** + * reads state for the local host from system keyspaces and creates an EndpointState, only to be used + * if we can't contact any peers during upgrade + */ + public static Map storedEpstate() + { + EndpointState epstate = new EndpointState(new HeartBeatState(SystemKeyspace.incrementAndGetGeneration(), 0)); + VersionedValue.VersionedValueFactory vf = StorageService.instance.valueFactory; + epstate.addApplicationState(DC, vf.datacenter(SystemKeyspace.getDatacenter())); + epstate.addApplicationState(RACK, vf.rack(SystemKeyspace.getRack())); + UUID hostId = SystemKeyspace.getLocalHostId(); + if (null != hostId) + { + epstate.addApplicationState(ApplicationState.HOST_ID, + StorageService.instance.valueFactory.hostId(hostId)); + } + Collection tokens = SystemKeyspace.getSavedTokens(); + epstate.addApplicationState(STATUS_WITH_PORT, vf.normal(tokens)); + epstate.addApplicationState(TOKENS, vf.tokens(tokens)); + epstate.addApplicationState(INTERNAL_ADDRESS_AND_PORT, vf.internalAddressAndPort(SystemKeyspace.getPreferredIP(FBUtilities.getLocalAddressAndPort()))); + epstate.addApplicationState(NATIVE_ADDRESS_AND_PORT, vf.nativeaddressAndPort(FBUtilities.getBroadcastNativeAddressAndPort())); + + Map epstates = new HashMap<>(); + epstates.put(FBUtilities.getBroadcastAddressAndPort(), epstate); + epstates.putAll(SystemKeyspace.peerEndpointStates()); + return epstates; + } + + @VisibleForTesting + public static ClusterMetadata fromEndpointStates(Map epStates, IPartitioner partitioner, DistributedSchema schema) + { + Directory directory = new Directory(); + TokenMap tokenMap = new TokenMap(partitioner); + List sortedEps = Lists.newArrayList(epStates.keySet()); + Collections.sort(sortedEps); + Map, ExtensionValue> extensions = new HashMap<>(); + for (InetAddressAndPort endpoint : sortedEps) + { + EndpointState epState = epStates.get(endpoint); + String dc = epState.getApplicationState(DC).value; + String rack = epState.getApplicationState(RACK).value; + String hostIdString = epState.getApplicationState(HOST_ID).value; + NodeAddresses nodeAddresses = getAddressesFromEndpointState(endpoint, epState); + NodeVersion nodeVersion = getVersionFromEndpointState(endpoint, epState); + assert hostIdString != null; + directory = directory.withNonUpgradedNode(nodeAddresses, + new Location(dc, rack), + nodeVersion, + toNodeState(endpoint, epState), + UUID.fromString(hostIdString)); + NodeId nodeId = directory.peerId(endpoint); + tokenMap = tokenMap.assignTokens(nodeId, getTokensIn(partitioner, epState)); + } + + ClusterMetadata forPlacementCalculation = new ClusterMetadata(Epoch.UPGRADE_GOSSIP, + Period.EMPTY, + true, + partitioner, + schema, + directory, + tokenMap, + DataPlacements.empty(), + LockedRanges.EMPTY, + InProgressSequences.EMPTY, + extensions); + return new ClusterMetadata(Epoch.UPGRADE_GOSSIP, + Period.EMPTY, + true, + partitioner, + schema, + directory, + tokenMap, + new UniformRangePlacement().calculatePlacements(Epoch.UPGRADE_GOSSIP, forPlacementCalculation, schema.getKeyspaces()), + LockedRanges.EMPTY, + InProgressSequences.EMPTY, + extensions); + } + + public static boolean isValidForClusterMetadata(Map epstates) + { + if (epstates.isEmpty()) + return false; + EnumSet requiredStates = EnumSet.of(DC, RACK, HOST_ID, TOKENS, RELEASE_VERSION); + for (Map.Entry entry : epstates.entrySet()) + { + EndpointState epstate = entry.getValue(); + for (ApplicationState state : requiredStates) + if (epstate.getApplicationState(state) == null) + { + logger.warn("Invalid endpoint state for {}; {} - {}", entry.getKey(), state, epstates); + return false; + } + } + return true; + } +} diff --git a/src/java/org/apache/cassandra/tcm/compatibility/TokenRingUtils.java b/src/java/org/apache/cassandra/tcm/compatibility/TokenRingUtils.java new file mode 100644 index 0000000000..3f4802a555 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/compatibility/TokenRingUtils.java @@ -0,0 +1,222 @@ +/* + * 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.tcm.compatibility; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Preconditions; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Iterators; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.ClusterMetadata; + +/** + * Functions for interacting with a sorted list of tokens as modelled as a ring + */ +public class TokenRingUtils +{ + private static final Logger logger = LoggerFactory.getLogger(TokenRingUtils.class); + + public static int firstTokenIndex(final List ring, Token start, boolean insertMin) + { + assert ring.size() > 0 : ring.toString(); + // insert the minimum token (at index == -1) if we were asked to include it and it isn't a member of the ring + int i = Collections.binarySearch(ring, start); + if (i < 0) + { + i = (i + 1) * (-1); + if (i >= ring.size()) + i = insertMin ? -1 : 0; + } + return i; + } + + public static Token firstToken(List ring, Token start) + { + return ring.get(firstTokenIndex(ring, start, false)); + } + + public static Token getPredecessor(List ring, Token start) + { + int idx = firstTokenIndex(ring, start, false); + return ring.get(idx == 0 ? ring.size() - 1 : idx - 1); + } + + public static Collection> getPrimaryRangesFor(List ring, Collection tokens) + { + Collection> ranges = new ArrayList<>(tokens.size()); + for (Token right : tokens) + ranges.add(new Range<>(getPredecessor(ring, right), right)); + return ranges; + } + + /** + * iterator over the Tokens in the given ring, starting with the token for the node owning start + * (which does not have to be a Token in the ring) + * @param includeMin True if the minimum token should be returned in the ring even if it has no owner. + */ + public static Iterator ringIterator(final List ring, Token start, boolean includeMin) + { + if (ring.isEmpty()) + return includeMin ? Iterators.singletonIterator(start.getPartitioner().getMinimumToken()) + : Collections.emptyIterator(); + + final boolean insertMin = includeMin && !ring.get(0).isMinimum(); + final int startIndex = firstTokenIndex(ring, start, insertMin); + return new AbstractIterator() + { + int j = startIndex; + protected Token computeNext() + { + if (j < -1) + return endOfData(); + try + { + // return minimum for index == -1 + if (j == -1) + return start.getPartitioner().getMinimumToken(); + // return ring token for other indexes + return ring.get(j); + } + finally + { + j++; + if (j == ring.size()) + j = insertMin ? -1 : 0; + if (j == startIndex) + // end iteration + j = -2; + } + } + }; + } + + /** + * Get the "primary ranges" for the specified keyspace and endpoint. + * "Primary ranges" are the ranges that the node is responsible for storing replica primarily. + * The node that stores replica primarily is defined as the first node returned + * by {@link AbstractReplicationStrategy#calculateNaturalReplicas}. + * + * @param keyspace Keyspace name to check primary ranges + * @param ep endpoint we are interested in. + * @return primary ranges for the specified endpoint. + */ + public static Collection> getPrimaryRangesForEndpoint(String keyspace, InetAddressAndPort ep) + { + AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); + Collection> primaryRanges = new HashSet<>(); + ClusterMetadata metadata = ClusterMetadata.current(); + List tokens = metadata.tokenMap.tokens(); + for (Token token : tokens) + { + EndpointsForRange replicas = strategy.calculateNaturalReplicas(token, metadata); + if (replicas.size() > 0 && replicas.get(0).endpoint().equals(ep)) + { + Preconditions.checkState(replicas.get(0).isFull()); + primaryRanges.add(new Range<>(getPredecessor(tokens, token), token)); + } + } + return primaryRanges; + } + + /** + * Get the "primary ranges" within local DC for the specified keyspace and endpoint. + * + * @see #getPrimaryRangesForEndpoint(String, InetAddressAndPort) + * @param keyspace Keyspace name to check primary ranges + * @param referenceEndpoint endpoint we are interested in. + * @return primary ranges within local DC for the specified endpoint. + */ + public static Collection> getPrimaryRangeForEndpointWithinDC(String keyspace, InetAddressAndPort referenceEndpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + String localDC = DatabaseDescriptor.getEndpointSnitch().getDatacenter(referenceEndpoint); + Collection localDcNodes = metadata.directory.datacenterEndpoints(localDC); + AbstractReplicationStrategy strategy = Keyspace.open(keyspace).getReplicationStrategy(); + + Collection> localDCPrimaryRanges = new HashSet<>(); + for (Token token : metadata.tokenMap.tokens()) + { + EndpointsForRange replicas = strategy.calculateNaturalReplicas(token, metadata); + for (Replica replica : replicas) + { + if (localDcNodes.contains(replica.endpoint())) + { + if (replica.endpoint().equals(referenceEndpoint)) + { + localDCPrimaryRanges.add(new Range<>(getPredecessor(metadata.tokenMap.tokens(), token), token)); + } + break; + } + } + } + + return localDCPrimaryRanges; + } + + /** + * Get all ranges that span the ring given a set + * of tokens. All ranges are in sorted order of + * ranges. + * @return ranges in sorted order + */ + public static List> getAllRanges(List sortedTokens) + { + if (logger.isTraceEnabled()) + logger.trace("computing ranges for {}", StringUtils.join(sortedTokens, ", ")); + + if (sortedTokens.isEmpty()) + return Collections.emptyList(); + int size = sortedTokens.size(); + List> ranges = new ArrayList<>(size + 1); + for (int i = 1; i < size; ++i) + { + Range range = new Range<>(sortedTokens.get(i - 1), sortedTokens.get(i)); + ranges.add(range); + } + Range range = new Range<>(sortedTokens.get(size - 1), sortedTokens.get(0)); + ranges.add(range); + + return ranges; + } + + public static Range getRange(List ring, Token token) + { + int idx = firstTokenIndex(ring, token, false); + Token replicaEnd = ring.get(idx); + Token replicaStart = ring.get(idx == 0 ? ring.size() - 1 : idx - 1); + return new Range<>(replicaStart, replicaEnd); + } +} diff --git a/src/java/org/apache/cassandra/tcm/extensions/AbstractExtensionValue.java b/src/java/org/apache/cassandra/tcm/extensions/AbstractExtensionValue.java new file mode 100644 index 0000000000..c23e6d0a8f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/extensions/AbstractExtensionValue.java @@ -0,0 +1,80 @@ +/* + * 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.tcm.extensions; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.Version; + +public abstract class AbstractExtensionValue implements ExtensionValue +{ + private Epoch lastModified = Epoch.EMPTY; + private V value = null; + + @Override + public V withLastModified(Epoch lastModified) + { + this.lastModified = lastModified; + return (V)this; + } + + public Epoch lastModified() + { + return lastModified; + } + + @Override + public void setValue(V value) + { + this.value = value; + } + + @Override + public V getValue() + { + return value; + } + + public void serialize(DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(lastModified(), out, version); + serializeInternal(out, version); + + } + abstract void serializeInternal(DataOutputPlus out, Version version) throws IOException; + + public void deserialize(DataInputPlus in, Version v) throws IOException + { + Epoch e = Epoch.serializer.deserialize(in, v); + withLastModified(e); + deserializeInternal(in, v); + } + abstract void deserializeInternal(DataInputPlus in, Version version) throws IOException; + + public long serializedSize(Version v) + { + long size = Epoch.serializer.serializedSize(lastModified(), v); + size += serializedSizeInternal(v); + return size; + } + abstract long serializedSizeInternal(Version v); +} diff --git a/src/java/org/apache/cassandra/tcm/extensions/EpochValue.java b/src/java/org/apache/cassandra/tcm/extensions/EpochValue.java new file mode 100644 index 0000000000..a01dc19401 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/extensions/EpochValue.java @@ -0,0 +1,54 @@ +/* + * 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.tcm.extensions; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.Version; + +public class EpochValue extends AbstractExtensionValue +{ + public static EpochValue create(Epoch epoch) + { + EpochValue v = new EpochValue(); + v.setValue(epoch); + return v; + } + + @Override + public void serializeInternal(DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(getValue(), out, version); + } + + @Override + public void deserializeInternal(DataInputPlus in, Version v) throws IOException + { + setValue(Epoch.serializer.deserialize(in, v)); + } + + @Override + public long serializedSizeInternal(Version v) + { + return Epoch.serializer.serializedSize(getValue(), v); + } +} diff --git a/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java b/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java new file mode 100644 index 0000000000..976e327951 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java @@ -0,0 +1,71 @@ +/* + * 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.tcm.extensions; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.MetadataKey; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; + +public class ExtensionKey> extends MetadataKey +{ + public static final Serializer serializer = new Serializer(); + public final Class valueType; + + public ExtensionKey(String id, Class valueType) + { + super(id); + this.valueType = valueType; + } + + public K newValue() + { + return valueType.cast(FBUtilities.construct(valueType.getName(), "extension value")); + } + + public static final class Serializer implements MetadataSerializer> + { + @Override + public void serialize(ExtensionKey t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.id); + out.writeUTF(t.valueType.getName()); + } + + @Override + public ExtensionKey deserialize(DataInputPlus in, Version version) throws IOException + { + String id = in.readUTF(); + String valType = in.readUTF(); + return new ExtensionKey(id, FBUtilities.classForName(valType, "value type")); + } + + @Override + public long serializedSize(ExtensionKey t, Version version) + { + return TypeSizes.sizeof(t.id) + TypeSizes.sizeof(t.valueType.getName()); + } + } +} + diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapReplacing.java b/src/java/org/apache/cassandra/tcm/extensions/ExtensionValue.java similarity index 58% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapReplacing.java rename to src/java/org/apache/cassandra/tcm/extensions/ExtensionValue.java index fea4e05247..0761571f0a 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapReplacing.java +++ b/src/java/org/apache/cassandra/tcm/extensions/ExtensionValue.java @@ -16,16 +16,21 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tcm.extensions; -import java.util.UUID; +import java.io.IOException; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingBootstrapReplacingRunner; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.serialization.Version; -class OnInstanceSetBootstrapReplacing extends ClusterReliableAction +public interface ExtensionValue extends MetadataValue { - OnInstanceSetBootstrapReplacing(ClusterActions actions, int on, int replacing, UUID hostId, String token) - { - super("Set " + on + " to Bootstrap Replacing", actions, on, addToRingBootstrapReplacingRunner(actions.cluster.get(on), actions.cluster.get(replacing), hostId, token)); - } + void setValue(V value); + V getValue(); + + void serialize(DataOutputPlus out, Version version) throws IOException; + void deserialize(DataInputPlus in, Version v) throws IOException; + long serializedSize(Version v); } diff --git a/src/java/org/apache/cassandra/tcm/extensions/IntValue.java b/src/java/org/apache/cassandra/tcm/extensions/IntValue.java new file mode 100644 index 0000000000..f4ac53c58c --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/extensions/IntValue.java @@ -0,0 +1,54 @@ +/* + * 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.tcm.extensions; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; + +public class IntValue extends AbstractExtensionValue +{ + public static IntValue create(int i) + { + IntValue v = new IntValue(); + v.setValue(i); + return v; + } + + @Override + void serializeInternal(DataOutputPlus out, Version version) throws IOException + { + out.writeInt(getValue()); + } + + @Override + void deserializeInternal(DataInputPlus in, Version version) throws IOException + { + setValue(in.readInt()); + } + + @Override + long serializedSizeInternal(Version v) + { + return TypeSizes.sizeof(getValue()); + } +} diff --git a/src/java/org/apache/cassandra/tcm/extensions/StringValue.java b/src/java/org/apache/cassandra/tcm/extensions/StringValue.java new file mode 100644 index 0000000000..79f8a824dc --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/extensions/StringValue.java @@ -0,0 +1,54 @@ +/* + * 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.tcm.extensions; + +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.Version; + +public class StringValue extends AbstractExtensionValue +{ + public static StringValue create(String s) + { + StringValue v = new StringValue(); + v.setValue(s); + return v; + } + + @Override + void serializeInternal(DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(getValue()); + } + + @Override + void deserializeInternal(DataInputPlus in, Version version) throws IOException + { + setValue(in.readUTF()); + } + + @Override + long serializedSizeInternal(Version v) + { + return TypeSizes.sizeof(getValue()); + } +} diff --git a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandlerFactory.java b/src/java/org/apache/cassandra/tcm/listeners/ChangeListener.java similarity index 54% rename from src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandlerFactory.java rename to src/java/org/apache/cassandra/tcm/listeners/ChangeListener.java index 2303663492..3f06d13531 100644 --- a/src/java/org/apache/cassandra/schema/DefaultSchemaUpdateHandlerFactory.java +++ b/src/java/org/apache/cassandra/tcm/listeners/ChangeListener.java @@ -16,21 +16,27 @@ * limitations under the License. */ -package org.apache.cassandra.schema; +package org.apache.cassandra.tcm.listeners; -import java.util.function.BiConsumer; +import org.apache.cassandra.tcm.ClusterMetadata; -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; - -public class DefaultSchemaUpdateHandlerFactory implements SchemaUpdateHandlerFactory +/** + * Invoked when cluster metadata changes + * + * `next` epoch is not guaranteed to directly follow `prev` epoch + */ +public interface ChangeListener { - public static final SchemaUpdateHandlerFactory instance = new DefaultSchemaUpdateHandlerFactory(); + /** + * Called before updating ClusterMetadata.current() - it is still `prev` here. + */ + default void notifyPreCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) {} + + /** + * Called after updating ClusterMetadata.current() - it is now `next` + */ + default void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) {} + + interface Async extends ChangeListener {} - @Override - public SchemaUpdateHandler getSchemaUpdateHandler(boolean online, BiConsumer updateSchemaCallback) - { - return online - ? new DefaultSchemaUpdateHandler(updateSchemaCallback) - : new OfflineSchemaUpdateHandler(updateSchemaCallback); - } } diff --git a/src/java/org/apache/cassandra/tcm/listeners/ClientNotificationListener.java b/src/java/org/apache/cassandra/tcm/listeners/ClientNotificationListener.java new file mode 100644 index 0000000000..8c3068a210 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/listeners/ClientNotificationListener.java @@ -0,0 +1,128 @@ +/* + * 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.tcm.listeners; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.Pair; + +import static org.apache.cassandra.tcm.listeners.ClientNotificationListener.ChangeType.LEAVE; +import static org.apache.cassandra.tcm.membership.NodeState.BOOTSTRAPPING; +import static org.apache.cassandra.tcm.membership.NodeState.MOVING; +import static org.apache.cassandra.tcm.membership.NodeState.REGISTERED; + +public class ClientNotificationListener implements ChangeListener +{ + private static final Logger logger = LoggerFactory.getLogger(ClientNotificationListener.class); + + /** + * notify clients when node JOIN/MOVE/LEAVE + * + * note that we don't register any listeners in StorageService until starting the native protocol + * so we won't send any notifications during startup replay (todo: should we start native before doing the background catchup?) + */ + @Override + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + List> diff = diff(prev.directory, next.directory); + logger.debug("Maybe notify listeners about {}", diff); + + for (Pair change : diff) + { + InetAddressAndPort endpoint = next.directory.endpoint(change.left); + switch (change.right) + { + case JOIN: + StorageService.instance.notifyJoined(endpoint); + break; + case MOVE: + StorageService.instance.notifyMoved(endpoint); + break; + case LEAVE: + StorageService.instance.notifyLeft(endpoint); + break; + } + } + } + + enum ChangeType + { + MOVE, + JOIN, + LEAVE + } + + private static List> diff(Directory prev, Directory next) + { + if (!prev.lastModified().equals(next.lastModified())) + { + List> changes = new ArrayList<>(); + for (NodeId node : next.peerIds()) + { + NodeState prevState = prev.peerState(node); + NodeState nextState = next.peerState(node); + ChangeType ct = fromNodeStateTransition(prevState, nextState); + if (ct != null) + changes.add(Pair.create(node, ct)); + } + return changes; + } + return Collections.emptyList(); + } + + static ChangeType fromNodeStateTransition(NodeState prev, NodeState next) + { + if (next == null) + return LEAVE; + if (prev == next) + return null; + switch (next) + { + case MOVING: + case LEAVING: + // if we see this NodeState but the previous state was null, it means we must have missed the JOINED state + if (prev == null || prev == BOOTSTRAPPING || prev == REGISTERED) + return ChangeType.JOIN; + return null; + case JOINED: + if (prev == MOVING) + return ChangeType.MOVE; + return ChangeType.JOIN; + case LEFT: + return LEAVE; + case BOOTSTRAPPING: + case BOOT_REPLACING: + case REGISTERED: + return null; + default: + throw new IllegalStateException("Unknown NodeState: " + next); + } + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSendShutdownToAll.java b/src/java/org/apache/cassandra/tcm/listeners/InitializationListener.java similarity index 60% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSendShutdownToAll.java rename to src/java/org/apache/cassandra/tcm/listeners/InitializationListener.java index fe62986fb2..5ce601d46e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSendShutdownToAll.java +++ b/src/java/org/apache/cassandra/tcm/listeners/InitializationListener.java @@ -16,18 +16,21 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tcm.listeners; -import org.apache.cassandra.simulator.Actions.ReliableAction; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.Entry; -import static org.apache.cassandra.simulator.Action.Modifiers.NONE; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; - -class OnInstanceSendShutdownToAll extends ReliableAction +public class InitializationListener implements LogListener { - OnInstanceSendShutdownToAll(ClusterActions actions, int from) + @Override + public void notify(Entry entry, Transformation.Result result) { - super("Send Shutdown from " + from + " to all", NONE, RELIABLE_NO_TIMEOUTS, - () -> actions.sendShutdownToAll(from)); + if (entry.transform.kind() == Transformation.Kind.INITIALIZE_CMS) + { + QueryProcessor.clearPreparedStatementsCache(); + QueryProcessor.clearInternalStatementsCache(); + } } } diff --git a/src/java/org/apache/cassandra/tcm/listeners/LegacyStateListener.java b/src/java/org/apache/cassandra/tcm/listeners/LegacyStateListener.java new file mode 100644 index 0000000000..0c72eef202 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/listeners/LegacyStateListener.java @@ -0,0 +1,164 @@ +/* + * 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.tcm.listeners; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.StreamSupport; + +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.virtual.PeersTable; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.compatibility.GossipHelper; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; + +import static org.apache.cassandra.gms.ApplicationState.SCHEMA; +import static org.apache.cassandra.tcm.membership.NodeState.BOOTSTRAPPING; +import static org.apache.cassandra.tcm.membership.NodeState.BOOT_REPLACING; +import static org.apache.cassandra.tcm.membership.NodeState.LEFT; +import static org.apache.cassandra.tcm.membership.NodeState.MOVING; + +public class LegacyStateListener implements ChangeListener.Async +{ + private static final Logger logger = LoggerFactory.getLogger(LegacyStateListener.class); + + @Override + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + if (!fromSnapshot && + next.directory.lastModified().equals(prev.directory.lastModified()) && + next.tokenMap.lastModified().equals(prev.tokenMap.lastModified())) + return; + + Set removed = Sets.difference(prev.directory.peerIds(), next.directory.peerIds()); + Set changed = new HashSet<>(); + for (NodeId node : next.directory.peerIds()) + { + if (directoryEntryChangedFor(node, prev.directory, next.directory) || !prev.tokenMap.tokens(node).equals(next.tokenMap.tokens(node))) + changed.add(node); + } + + for (NodeId remove : removed) + { + GossipHelper.evictFromMembership(prev.directory.endpoint(remove)); + PeersTable.updateLegacyPeerTable(remove, prev, next); + } + + for (NodeId change : changed) + { + // next.myNodeId() can be null during replay (before we have registered) + if (next.myNodeId() != null && next.myNodeId().equals(change)) + { + switch (next.directory.peerState(change)) + { + case BOOTSTRAPPING: + if (prev.directory.peerState(change) != BOOTSTRAPPING) + { + // legacy log messages for tests + logger.info("JOINING: Starting to bootstrap"); + logger.info("JOINING: calculation complete, ready to bootstrap"); + } + break; + case BOOT_REPLACING: + case REGISTERED: + break; + case JOINED: + SystemKeyspace.updateTokens(next.directory.endpoint(change), next.tokenMap.tokens(change)); + // needed if we miss the REGISTERED above; Does nothing if we are already in epStateMap: + Gossiper.instance.maybeInitializeLocalState(SystemKeyspace.incrementAndGetGeneration()); + StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false) + .filter(cfs -> Schema.instance.getUserKeyspaces().names().contains(cfs.keyspace.getName())) + .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(true)); + if (prev.directory.peerState(change) == MOVING) + logger.info("Node {} state jump to NORMAL", next.directory.endpoint(change)); + break; + } + // Maybe intitialise local epstate whatever the node state because we could be processing after a + // replay and so may have not seen any previous local states, making this the first mutation of gossip + // state for the local node. + Gossiper.instance.maybeInitializeLocalState(SystemKeyspace.incrementAndGetGeneration()); + Gossiper.instance.addLocalApplicationState(SCHEMA, StorageService.instance.valueFactory.schema(next.schema.getVersion())); + } + + if (next.directory.peerState(change) == LEFT) + { + Gossiper.instance.mergeNodeToGossip(change, next, prev.tokenMap.tokens(change)); + InetAddressAndPort endpoint = prev.directory.endpoint(change); + if (endpoint != null) + { + PeersTable.updateLegacyPeerTable(change, prev, next); + GossipHelper.removeFromGossip(endpoint); + } + } + else if (NodeState.isBootstrap(next.directory.peerState(change))) + { + // For compatibility with clients, ensure we set TOKENS for bootstrapping nodes in gossip. + // As these are not yet added to the token map they must be extracted from the in progress sequence. + Collection tokens = GossipHelper.getTokensFromOperation(change, next); + Gossiper.instance.mergeNodeToGossip(change, next, tokens); + } + else if (prev.directory.peerState(change) == BOOT_REPLACING) + { + // legacy log message for compatibility (& tests) + MultiStepOperation sequence = prev.inProgressSequences.get(change); + if (sequence != null && sequence.kind() == MultiStepOperation.Kind.REPLACE) + { + BootstrapAndReplace replace = (BootstrapAndReplace) sequence; + InetAddressAndPort replaced = prev.directory.endpoint(replace.startReplace.replaced()); + InetAddressAndPort replacement = prev.directory.endpoint(change); + Collection tokens = GossipHelper.getTokensFromOperation(replace); + logger.info("Node {} will complete replacement of {} for tokens {}", replacement, replaced, tokens); + if (!replacement.equals(replaced)) + { + for (Token token : tokens) + logger.warn("Token {} changing ownership from {} to {}", token, replaced, replacement); + } + } + } + else + { + Gossiper.instance.mergeNodeToGossip(change, next); + PeersTable.updateLegacyPeerTable(change, prev, next); + } + } + } + + private boolean directoryEntryChangedFor(NodeId nodeId, Directory prev, Directory next) + { + return prev.peerState(nodeId) != next.peerState(nodeId) || + !Objects.equals(prev.getNodeAddresses(nodeId), next.getNodeAddresses(nodeId)) || + !Objects.equals(prev.version(nodeId), next.version(nodeId)); + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeft.java b/src/java/org/apache/cassandra/tcm/listeners/LogListener.java similarity index 69% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeft.java rename to src/java/org/apache/cassandra/tcm/listeners/LogListener.java index 83de795708..0b172d613c 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeft.java +++ b/src/java/org/apache/cassandra/tcm/listeners/LogListener.java @@ -16,14 +16,18 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tcm.listeners; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingLeftRunner; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.Entry; -class OnInstanceSetLeft extends ClusterReliableAction +/** + * Invoked each time the log is updated + * + * Not guaranteed to see all transformations + */ +public interface LogListener { - OnInstanceSetLeft(ClusterActions actions, int on) - { - super("Set " + on + " to Left", actions, on, addToRingLeftRunner(actions.cluster.get(on))); - } + void notify(Entry entry, Transformation.Result result); } + diff --git a/src/java/org/apache/cassandra/tcm/listeners/MetadataSnapshotListener.java b/src/java/org/apache/cassandra/tcm/listeners/MetadataSnapshotListener.java new file mode 100644 index 0000000000..3a2e6a4ec0 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/listeners/MetadataSnapshotListener.java @@ -0,0 +1,51 @@ +/* + * 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.tcm.listeners; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.Entry; + +public class MetadataSnapshotListener implements LogListener +{ + private static final Logger logger = LoggerFactory.getLogger(MetadataSnapshotListener.class); + @Override + public void notify(Entry entry, Transformation.Result result) + { + ClusterMetadata next = result.success().metadata; + if (entry.transform.kind() == Transformation.Kind.SEAL_PERIOD) + { + assert next.lastInPeriod; + try + { + ClusterMetadataService.instance().snapshotManager().storeSnapshot(next); + Sealed.recordSealedPeriod(next.period, next.epoch); + } + catch (Throwable e) + { + logger.warn("Unable to serialize metadata snapshot triggered by SealPeriod transformation", e); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/listeners/PlacementsChangeListener.java b/src/java/org/apache/cassandra/tcm/listeners/PlacementsChangeListener.java new file mode 100644 index 0000000000..80da4ec844 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/listeners/PlacementsChangeListener.java @@ -0,0 +1,52 @@ +/* + * 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.tcm.listeners; + +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; + +public class PlacementsChangeListener implements ChangeListener +{ + @Override + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + if (shouldInvalidate(prev, next)) + StorageService.instance.invalidateLocalRanges(); + } + + private boolean shouldInvalidate(ClusterMetadata prev, ClusterMetadata next) + { + if (!prev.placements.lastModified().equals(next.placements.lastModified()) && + !prev.placements.equals(next.placements)) // <- todo should we update lastModified if the result is the same? + return true; + + if (prev.schema.getKeyspaces().size() != next.schema.getKeyspaces().size()) + return true; + + // can't rely only on placements alone since we can move a ks from rf=1 to rf=3 and the rf=3 params might already exist in the placements: + for (KeyspaceMetadata ksm : prev.schema.getKeyspaces()) + { + KeyspaceMetadata newKsm = next.schema.getKeyspaceMetadata(ksm.name); + if (newKsm == null || !ksm.params.equals(newKsm.params)) + return true; + } + return false; + } +} diff --git a/src/java/org/apache/cassandra/tcm/listeners/SchemaListener.java b/src/java/org/apache/cassandra/tcm/listeners/SchemaListener.java new file mode 100644 index 0000000000..2787f5eb2c --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/listeners/SchemaListener.java @@ -0,0 +1,57 @@ +/* + * 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.tcm.listeners; + +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaDiagnostics; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; + +public class SchemaListener implements ChangeListener +{ + @Override + public void notifyPreCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + notifyInternal(prev, next, fromSnapshot, true); + } + + protected void notifyInternal(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot, boolean loadSSTables) + { + if (!fromSnapshot && next.schema.lastModified().equals(prev.schema.lastModified())) + return; + + next.schema.initializeKeyspaceInstances(prev.schema, loadSSTables); + } + + @Override + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + if (!fromSnapshot && next.schema.lastModified().equals(prev.schema.lastModified())) + return; + + DistributedSchema.maybeRebuildViews(prev.schema, next.schema); + SchemaDiagnostics.versionUpdated(Schema.instance); + Gossiper.instance.addLocalApplicationState(ApplicationState.SCHEMA, StorageService.instance.valueFactory.schema(next.schema.getVersion())); + SystemKeyspace.updateSchemaVersion(next.schema.getVersion()); + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetNormal.java b/src/java/org/apache/cassandra/tcm/listeners/UpgradeMigrationListener.java similarity index 52% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetNormal.java rename to src/java/org/apache/cassandra/tcm/listeners/UpgradeMigrationListener.java index cc3702ca13..ff24e17b9d 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetNormal.java +++ b/src/java/org/apache/cassandra/tcm/listeners/UpgradeMigrationListener.java @@ -16,21 +16,24 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tcm.listeners; -import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingNormalRunner; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; -class OnInstanceSetNormal extends ClusterReliableAction +public class UpgradeMigrationListener implements ChangeListener.Async { - OnInstanceSetNormal(ClusterActions actions, int on, UUID hostId, String tokenString) + private static final Logger logger = LoggerFactory.getLogger(UpgradeMigrationListener.class); + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) { - super("Set " + on + " to Normal", actions, on, addToRingNormalRunner(actions.cluster.get(on), hostId, tokenString)); - } + if (!prev.epoch.equals(Epoch.UPGRADE_GOSSIP)) + return; - OnInstanceSetNormal(ClusterActions actions, int on) - { - super("Set " + on + " to Normal", actions, on, addToRingNormalRunner(actions.cluster.get(on))); + logger.info("Detected upgrade from gossip mode, updating my host id in gossip to {}", next.myNodeId()); + Gossiper.instance.mergeNodeToGossip(next.myNodeId(), next); } } diff --git a/src/java/org/apache/cassandra/tcm/log/Entry.java b/src/java/org/apache/cassandra/tcm/log/Entry.java new file mode 100644 index 0000000000..c7eb350efc --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/Entry.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.log; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; + +public class Entry implements Comparable +{ + public static final Serializer serializer = new Serializer(); + + public final Id id; + public final Epoch epoch; + public final Transformation transform; + + public Entry(Id id, Epoch epoch, Transformation transform) + { + this.id = id; + this.epoch = epoch; + this.transform = transform; + } + + public Entry maybeUnwrapExecuted() + { + if (transform instanceof Transformation.Executed) + return new Entry(id, epoch, ((Transformation.Executed) transform).original()); + + return this; + } + + @Override + public int compareTo(Entry other) + { + return this.epoch.compareTo(other.epoch); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Entry)) return false; + Entry that = (Entry) o; + return Objects.equals(id, that.id) && Objects.equals(epoch, that.epoch) && Objects.equals(transform, that.transform); + } + + public int hashCode() + { + return Objects.hash(id, epoch, transform); + } + + public String toString() + { + return "Entry{" + + "id=" + id + + ", epoch=" + epoch + + ", transform=" + transform + + '}'; + } + + static final class Serializer implements MetadataSerializer + { + public void serialize(Entry t, DataOutputPlus out, Version version) throws IOException + { + Id.serializer.serialize(t.id, out, version); + Epoch.serializer.serialize(t.epoch, out, version); + Transformation.transformationSerializer.serialize(t.transform, out, version); + } + + public Entry deserialize(DataInputPlus in, Version version) throws IOException + { + Id entryId = Id.serializer.deserialize(in, version); + Epoch epoch = Epoch.serializer.deserialize(in, version); + Transformation transform = Transformation.transformationSerializer.deserialize(in, version); + return new Entry(entryId, epoch, transform); + } + + public long serializedSize(Entry t, Version version) + { + return Id.serializer.serializedSize(t.id, version) + + Epoch.serializer.serializedSize(t.epoch, version) + + Transformation.transformationSerializer.serializedSize(t.transform, version); + } + } + public static class Id + { + public static final EntryIdSerializer serializer = new EntryIdSerializer(); + public static final Id NONE = new Id(-1L); + + public final long entryId; + + public Id(long entryId) + { + this.entryId = entryId; + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id groupId = (Id) o; + return entryId == groupId.entryId; + } + + public int hashCode() + { + return Objects.hash(entryId); + } + + public String toString() + { + return "EntryId{" + + "entryId=" + entryId + + '}'; + } + + public static class EntryIdSerializer implements MetadataSerializer + { + public void serialize(Id id, DataOutputPlus out, Version version) throws IOException + { + out.writeLong(id.entryId); + } + + public Id deserialize(DataInputPlus in, Version version) throws IOException + { + return new Id(in.readLong()); + } + + public long serializedSize(Id t, Version version) + { + return TypeSizes.LONG_SIZE; + } + } + } + + public static class DefaultEntryIdGen implements Supplier + { + private final AtomicLong counter = new AtomicLong(Clock.Global.currentTimeMillis() & 0x00000000ffffffffL); + private final long addrComponent; + + public DefaultEntryIdGen() + { + this (FBUtilities.getBroadcastAddressAndPort()); + } + + public DefaultEntryIdGen(InetAddressAndPort addr) + { + // TODO properly handle ipv6 + byte[] bytes = addr.addressBytes; + long addrComponent = 0; + for (int i = 0; i < bytes.length; i++) + addrComponent |= (long) bytes[i] << (i * 8); + this.addrComponent = addrComponent << Integer.SIZE; + } + + public Id get() + { + return new Id(addrComponent | counter.getAndIncrement()); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/log/LocalLog.java b/src/java/org/apache/cassandra/tcm/log/LocalLog.java new file mode 100644 index 0000000000..9f21dc9f03 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/LocalLog.java @@ -0,0 +1,858 @@ +/* + * 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.tcm.log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.Interruptible; +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.DurationSpec; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.listeners.ChangeListener; +import org.apache.cassandra.tcm.listeners.ClientNotificationListener; +import org.apache.cassandra.tcm.listeners.InitializationListener; +import org.apache.cassandra.tcm.listeners.LegacyStateListener; +import org.apache.cassandra.tcm.listeners.LogListener; +import org.apache.cassandra.tcm.listeners.MetadataSnapshotListener; +import org.apache.cassandra.tcm.listeners.PlacementsChangeListener; +import org.apache.cassandra.tcm.listeners.SchemaListener; +import org.apache.cassandra.tcm.listeners.UpgradeMigrationListener; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; +import org.apache.cassandra.tcm.transformations.cms.PreInitialize; +import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.concurrent.Condition; +import org.apache.cassandra.utils.concurrent.WaitQueue; + +import static java.util.Comparator.comparing; +import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; +import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; +import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; +import static org.apache.cassandra.tcm.Epoch.EMPTY; +import static org.apache.cassandra.tcm.Epoch.FIRST; +import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue; + +// TODO metrics for contention/buffer size/etc + +/** + * LocalLog is an entity responsible for collecting replicated entries and enacting new epochs locally as soon + * as the node has enough information to reconstruct ClusterMetadata instance at that epoch. + * + * Since ClusterMetadata can be replicated to the node by different means (commit response, replication after + * commit by other node, CMS or peer log replay), it may happen that replicated entries arrive out-of-order + * and may even contain gaps. For example, if node1 has registered at epoch 10, and node2 has registered at + * epoch 11, it may happen that node3 will receive entry for epoch 11 before it receives the entry for epoch 10. + * To reconstruct the history, LocalLog has a reorder buffer, which holds entries until the one that is consecutive + * to the highest known epoch is available, at which point it (and all subsequent entries whose predecessors appear in the + * pending buffer) is enacted. + */ +public abstract class LocalLog implements Closeable +{ + private static final Logger logger = LoggerFactory.getLogger(LocalLog.class); + + protected final AtomicReference committed; + // Indicates that, during process startup, the intial replay of persisted log entries has been performed + // and the log made ready for use. This involves adding the listeners and firing a one time post-commit + // notification to them all. + private final AtomicBoolean replayComplete = new AtomicBoolean(); + + public static class LogSpec + { + public enum WhenReady { NONE, PRE_COMMIT_ONLY, POST_COMMIT_ONLY, ALL}; + + private ClusterMetadata initial = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + private LogStorage storage = LogStorage.None; + private boolean defaultListeners = false; + private boolean reset = false; + private WhenReady whenReady = WhenReady.POST_COMMIT_ONLY; + + private final Set listeners = new HashSet<>(); + private final Set changeListeners = new HashSet<>(); + private final Set asyncChangeListeners = new HashSet<>(); + + public LogSpec withDefaultListeners() + { + return withDefaultListeners(true); + } + + public LogSpec withDefaultListeners(boolean withDefaultListeners) + { + if (withDefaultListeners && + !(listeners.isEmpty() && changeListeners.isEmpty() && asyncChangeListeners.isEmpty())) + { + throw new IllegalStateException("LogSpec can only require all listeners OR specific listeners"); + } + + defaultListeners = withDefaultListeners; + return this; + } + + public LogSpec withLogListener(LogListener listener) + { + if (defaultListeners) + throw new IllegalStateException("LogSpec can only require all listeners OR specific listeners"); + listeners.add(listener); + return this; + } + + public LogSpec withListener(ChangeListener listener) + { + if (defaultListeners) + throw new IllegalStateException("LogSpec can only require all listeners OR specific listeners"); + if (listener instanceof ChangeListener.Async) + asyncChangeListeners.add((ChangeListener.Async) listener); + else + changeListeners.add(listener); + return this; + } + + public LogSpec isReset(boolean isReset) + { + reset = isReset; + return this; + } + + public boolean isReset() + { + return reset; + } + + public LogSpec withStorage(LogStorage storage) + { + this.storage = storage; + return this; + } + + public LogSpec withInitialState(ClusterMetadata initial) + { + this.initial = initial; + return this; + } + + public LogSpec withReadyNotification(WhenReady whenReady) + { + this.whenReady = whenReady; + return this; + } + } + + /** + * Custom comparator for pending entries. In general, we would like entries in the pending set to be ordered by epoch, + * from smallest to highest, so that `#first()` call would return the smallest entry. + * + * However, snapshots should be applied out of order, and snapshots with higher epoch should be applied before snapshots + * with a lower epoch in cases when there are multiple snapshots present. + */ + protected final ConcurrentSkipListSet pending = new ConcurrentSkipListSet<>((Entry e1, Entry e2) -> { + if (e1.transform.kind() == Transformation.Kind.FORCE_SNAPSHOT && e2.transform.kind() == Transformation.Kind.FORCE_SNAPSHOT) + return e2.epoch.compareTo(e1.epoch); + + if (e1.transform.kind() == Transformation.Kind.FORCE_SNAPSHOT) + return -1; + + if (e2.transform.kind() == Transformation.Kind.FORCE_SNAPSHOT) + return 1; + + return e1.epoch.compareTo(e2.epoch); + }); + + protected final LogStorage persistence; + protected final Set listeners; + protected final Set changeListeners; + protected final Set asyncChangeListeners; + private final LogSpec spec; + + private LocalLog(LogSpec spec) + { + assert spec.initial.epoch.is(EMPTY) || spec.initial.epoch.is(Epoch.UPGRADE_STARTUP) || spec.reset; + committed = new AtomicReference<>(spec.initial); + this.persistence = spec.storage; + listeners = Sets.newConcurrentHashSet(); + changeListeners = Sets.newConcurrentHashSet(); + asyncChangeListeners = Sets.newConcurrentHashSet(); + this.spec = spec; + } + + public void bootstrap(InetAddressAndPort addr) + { + ClusterMetadata metadata = metadata(); + assert metadata.epoch.isBefore(FIRST) : String.format("Metadata epoch %s should be before first", metadata.epoch); + Transformation transform = PreInitialize.withFirstCMS(addr); + append(new Entry(Entry.Id.NONE, FIRST, transform)); + waitForHighestConsecutive(); + metadata = metadata(); + assert metadata.epoch.is(Epoch.FIRST) : String.format("Epoch: %s. CMS: %s", metadata.epoch, metadata.fullCMSMembers()); + } + + public ClusterMetadata metadata() + { + return committed.get(); + } + + public boolean unsafeSetCommittedFromGossip(ClusterMetadata expected, ClusterMetadata updated) + { + if (!(expected.epoch.isEqualOrBefore(Epoch.UPGRADE_GOSSIP) && updated.epoch.is(Epoch.UPGRADE_GOSSIP))) + throw new IllegalStateException(String.format("Illegal epochs for setting from gossip; expected: %s, updated: %s", + expected.epoch, updated.epoch)); + return committed.compareAndSet(expected, updated); + } + + public void unsafeSetCommittedFromGossip(ClusterMetadata updated) + { + if (!updated.epoch.is(Epoch.UPGRADE_GOSSIP)) + throw new IllegalStateException(String.format("Illegal epoch for setting from gossip; updated: %s", + updated.epoch)); + committed.set(updated); + } + + public int pendingBufferSize() + { + return pending.size(); + } + + public static LocalLog sync(LogSpec spec) + { + return new Sync(spec); + } + + public static LocalLog async(LogSpec spec) + { + return new Async(spec); + } + + @VisibleForTesting + public static LocalLog asyncForTests() + { + LogSpec logSpec = new LogSpec(); + LocalLog log = new Async(logSpec); + log.ready(); + return log; + } + + @VisibleForTesting + public static LocalLog asyncForTests(ClusterMetadata initial) + { + LogSpec logSpec = new LogSpec().withInitialState(initial); + LocalLog log = new Async(logSpec); + log.ready(); + return log; + } + + public boolean hasGaps() + { + Epoch start = committed.get().epoch; + for (Entry entry : pending) + { + if (!entry.epoch.isDirectlyAfter(start)) + return true; + else + start = entry.epoch; + } + return false; + } + + public Optional highestPending() + { + try + { + return Optional.of(pending.last().epoch); + } + catch (NoSuchElementException eag) + { + return Optional.empty(); + } + } + + public Replication getCommittedEntries(Epoch since) + { + return persistence.getReplication(since); + } + + public ClusterMetadata waitForHighestConsecutive() + { + runOnce(); + return metadata(); + } + + public void append(Collection entries) + { + if (!entries.isEmpty()) + { + if (logger.isDebugEnabled()) + logger.debug("Appending entries to the pending buffer: {}", entries.stream().map(e -> e.epoch).collect(Collectors.toList())); + pending.addAll(entries); + processPending(); + } + } + + public void append(Entry entry) + { + logger.debug("Appending entry to the pending buffer: {}", entry.epoch); + pending.add(entry); + processPending(); + } + + /** + * Append log state snapshot. Does _not_ give any guarantees about visibility of the highest consecutive epoch. + */ + public void append(LogState logState) + { + logger.debug("Appending log state with snapshot to the pending buffer: {}", logState); + // If we receive a base state (snapshot), we need to construct a synthetic ForceSnapshot transformation that will serve as + // a base for application of the rest of the entries. If the log state contains any additional transformations that follow + // the base state, we can simply apply them to the log after. + if (logState.baseState != null) + { + Epoch epoch = logState.baseState.epoch; + + // Create a synthetic "force snapshot" transformation to instruct the log to pick up given metadata + ForceSnapshot transformation = new ForceSnapshot(logState.baseState); + Entry newEntry = new Entry(Entry.Id.NONE, epoch, transformation); + pending.add(newEntry); + } + + // Finally, append any additional transformations in the snapshot. Some or all of these could be earlier than the + // currently enacted epoch (if we'd already moved on beyond the epoch of the base state for instance, or if newer + // entries have been received via normal replication), but this is fine as entries will be put in the reorder + // log, and duplicates will be dropped. + pending.addAll(logState.transformations.entries()); + processPending(); + } + + public abstract ClusterMetadata awaitAtLeast(Epoch epoch) throws InterruptedException, TimeoutException; + + /** + * Makes sure that the pending queue is processed _at least once_. + */ + void runOnce() + { + try + { + runOnce(null); + } + catch (InterruptedException | TimeoutException e) + { + throw new RuntimeException("Should not have happened, since we await uninterruptibly", e); + } + } + + abstract void runOnce(DurationSpec durationSpec) throws InterruptedException, TimeoutException; + abstract void processPending(); + + private Entry peek() + { + try + { + return pending.first(); + } + catch (NoSuchElementException ignore) + { + return null; + } + } + + /** + * Called by implementations of {@link #processPending()}. + * + * Implementations have to guarantee there can be no more than one caller of {@link #processPendingInternal()} + * at a time, as we are making calls to pre- and post- commit hooks. In other words, this method should be called + * _exclusively_ from the implementation, outside of it there's no way to ensure mutual exclusion without + * additional guards. + * + * Please note that we are using a custom comparator for pending entries, which ensures that FORCE_SNAPSHOT entries + * are going to be prioritised over other entry kinds. After application of the snapshot entry, any entry with epoch + * lower than the one that snapshot has enacted, are simply going to be dropped. The rest of entries (i.e. ones + * that have epoch higher than the snapshot entry), are going to be processed in a regular fashion. + */ + void processPendingInternal() + { + while (true) + { + Entry pendingEntry = peek(); + + if (pendingEntry == null) + return; + + ClusterMetadata prev = committed.get(); + // ForceSnapshot + Bootstrap entries can "jump" epoch + boolean isPreInit = pendingEntry.transform.kind() == Transformation.Kind.PRE_INITIALIZE_CMS; + boolean isSnapshot = pendingEntry.transform.kind() == Transformation.Kind.FORCE_SNAPSHOT; + if (pendingEntry.epoch.isDirectlyAfter(prev.epoch) + || ((isPreInit || isSnapshot) && pendingEntry.epoch.isAfter(prev.epoch))) + { + try + { + Transformation.Result transformed; + + try + { + transformed = pendingEntry.transform.execute(prev); + } + catch (Throwable t) + { + logger.error(String.format("Caught an exception while processing entry %s. This can mean that this node is configured differently from CMS.", prev), t); + throw new StopProcessingException(t); + } + + if (!transformed.isSuccess()) + { + logger.error("Error while processing entry {}. Transformation returned result of {}. This can mean that this node is configured differently from CMS.", prev, transformed.rejected()); + throw new StopProcessingException(); + } + + ClusterMetadata next = transformed.success().metadata; + assert pendingEntry.epoch.is(next.epoch) : + String.format("Entry epoch %s does not match metadata epoch %s", pendingEntry.epoch, next.epoch); + assert next.epoch.isDirectlyAfter(prev.epoch) || isSnapshot || pendingEntry.transform.kind() == Transformation.Kind.PRE_INITIALIZE_CMS : + String.format("Epoch %s for %s can either force snapshot, or immediately follow %s", + next.epoch, pendingEntry.transform, prev.epoch); + + if (replayComplete.get()) + persistence.append(transformed.success().metadata.period, pendingEntry.maybeUnwrapExecuted()); + + notifyPreCommit(prev, next, isSnapshot); + + if (committed.compareAndSet(prev, next)) + { + logger.info("Enacted {}. New tail is {}", pendingEntry.transform, next.epoch); + maybeNotifyListeners(pendingEntry, transformed); + } + else + { + // Since we disallow concurrent calls to `processPendingInternal` (as declared in the interface), + // we might have made an erroneous extra initialization of keyspaces by now, and, unless we + // throw here, we may in addition call to `afterCommit`. + throw new IllegalStateException(String.format("CAS conflict while trying to commit entry with seq %s, old version tail: %s current version tail: %s", + next.epoch, prev.epoch, metadata().epoch)); + } + + notifyPostCommit(prev, next, isSnapshot); + } + catch (StopProcessingException t) + { + throw t; + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.error("Could not process the entry", t); + } + finally + { + // if we did succeed performing the commit, or have experienced an exception, remove from the buffer + pending.remove(pendingEntry); + } + } + else if (!pendingEntry.epoch.isAfter(metadata().epoch)) + { + logger.debug(String.format("An already appended entry %s discovered in the pending buffer, ignoring. Max consecutive: %s", + pendingEntry.epoch, prev.epoch)); + pending.remove(pendingEntry); + } + else + { + Entry tmp = pending.first(); + if (tmp.epoch.is(pendingEntry.epoch)) + { + logger.debug("Smallest entry is non-consecutive {} to {}", pendingEntry.epoch, prev.epoch); + // if this one was not consecutive, subsequent won't be either + return; + } + } + } + } + + /** + * Replays items that were persisted during previous starts. Replayed items _will not_ be persisted again. + */ + public Epoch replayPersisted() + { + if (replayComplete.get()) + throw new IllegalStateException("Can only replay persisted once."); + LogState logState = persistence.getLogState(metadata().epoch); + append(logState); + return waitForHighestConsecutive().epoch; + } + + private void maybeNotifyListeners(Entry entry, Transformation.Result result) + { + for (LogListener listener : listeners) + listener.notify(entry, result); + } + + public void addListener(LogListener listener) + { + this.listeners.add(listener); + } + + public void addListener(ChangeListener listener) + { + if (listener instanceof ChangeListener.Async) + this.asyncChangeListeners.add((ChangeListener.Async) listener); + else + this.changeListeners.add(listener); + } + + public void removeListener(ChangeListener listener) + { + this.changeListeners.remove(listener); + } + + public void notifyListeners(ClusterMetadata emptyFromSystemTables) + { + ClusterMetadata metadata = ClusterMetadata.current(); + notifyPreCommit(emptyFromSystemTables, metadata, true); + notifyPostCommit(emptyFromSystemTables, metadata, true); + } + + private void notifyPreCommit(ClusterMetadata before, ClusterMetadata after, boolean fromSnapshot) + { + for (ChangeListener listener : changeListeners) + listener.notifyPreCommit(before, after, fromSnapshot); + for (ChangeListener.Async listener : asyncChangeListeners) + ScheduledExecutors.optionalTasks.submit(() -> listener.notifyPreCommit(before, after, fromSnapshot)); + } + + private void notifyPostCommit(ClusterMetadata before, ClusterMetadata after, boolean fromSnapshot) + { + for (ChangeListener listener : changeListeners) + listener.notifyPostCommit(before, after, fromSnapshot); + for (ChangeListener.Async listener : asyncChangeListeners) + ScheduledExecutors.optionalTasks.submit(() -> listener.notifyPostCommit(before, after, fromSnapshot)); + } + + + private static class Async extends LocalLog + { + private final AsyncRunnable runnable; + private final Interruptible executor; + + private Async(LogSpec spec) + { + super(spec); + this.runnable = new AsyncRunnable(); + this.executor = ExecutorFactory.Global.executorFactory().infiniteLoop("GlobalLogFollower", runnable, SAFE, NON_DAEMON, UNSYNCHRONIZED); + } + + @Override + public ClusterMetadata awaitAtLeast(Epoch epoch) throws InterruptedException, TimeoutException + { + ClusterMetadata lastSeen = committed.get(); + return lastSeen.epoch.compareTo(epoch) >= 0 + ? lastSeen + : new AwaitCommit(epoch).get(); + } + + @Override + public void runOnce(DurationSpec duration) throws InterruptedException, TimeoutException + { + Condition ours = Condition.newOneTimeCondition(); + for (int i = 0; i < 2; i++) + { + Condition current = runnable.subscriber.get(); + + // If another thread has already initiated the follower runnable to execute, this will be non-null. + // If so, we'll wait for it to ensure that the inflight, partial execution of the runnable's loop is + // complete. + if (current != null) + { + if (duration == null) + { + current.awaitUninterruptibly(); + } + else if (!current.await(duration.to(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)) + { + throw new TimeoutException(String.format("Timed out waiting for follower to run at least once. " + + "Pending is %s and current is now at epoch %s.", + pending.stream().map((re) -> re.epoch).collect(Collectors.toList()), + metadata().epoch)); + } + } + + // Either the runnable was already running (but we cannot know at what point in its processing it + // was when we started to wait on the current condition), or the runnable was not running when we + // entered this loop. + // If the CAS here succeeds, we know that waiting on our condition will guarantee a full + // execution of the runnable. + // If we fail to CAS, that's also ok as it means another thread beat us to it and we can just go around + // again and wait for the condition _it_ set to complete as this also guarantees a full execution of the + // runnable's loop. + + // If we reach this point on our second iteration we can exit, even if current was null both times + // as it means that the condition we lost the CAS to on the first iteration has completed and therefore + // a full execution of the runnable has completed. + if (i == 1) + return; + + if (runnable.subscriber.compareAndSet(null, ours)) + { + runnable.logNotifier.signalAll(); + ours.awaitUninterruptibly(); + return; + } + } + } + + @Override + void processPending() + { + runnable.logNotifier.signalAll(); + } + + @Override + public void close() + { + executor.shutdownNow(); + + Condition condition = runnable.subscriber.get(); + if (condition != null) + condition.signalAll(); + runnable.logNotifier.signalAll(); + try + { + executor.awaitTermination(30, TimeUnit.SECONDS); + } + catch (InterruptedException e) + { + logger.error(e.getMessage(), e); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + class AsyncRunnable implements Interruptible.Task + { + private final AtomicReference subscriber; + private final WaitQueue logNotifier; + + private AsyncRunnable() + { + this.logNotifier = newWaitQueue(); + subscriber = new AtomicReference<>(); + } + + public void run(Interruptible.State state) throws InterruptedException + { + try + { + if (state != Interruptible.State.SHUTTING_DOWN) + { + Condition condition = subscriber.getAndSet(null); + // Grab a ticket ahead of time, so that we can't get into race with the exit from process pending + WaitQueue.Signal signal = logNotifier.register(); + processPendingInternal(); + if (condition != null) + condition.signalAll(); + // if no new threads have subscribed since we started running, await + // otherwise, run again to process whatever work they may be waiting on + if (subscriber.get() == null) + signal.await(); + } + } + catch (StopProcessingException t) + { + logger.warn("Stopping log processing on the node... All subsequent epochs will be ignored.", t); + executor.shutdown(); + } + catch (InterruptedException t) + { + // ignore + } + catch (Throwable t) + { + // TODO handle properly + logger.warn("Error in log follower", t); + } + } + } + + private class AwaitCommit + { + private final Epoch waitingFor; + + private AwaitCommit(Epoch waitingFor) + { + this.waitingFor = waitingFor; + } + + public ClusterMetadata get() throws InterruptedException, TimeoutException + { + return get(DatabaseDescriptor.getCmsAwaitTimeout()); + } + + public ClusterMetadata get(DurationSpec duration) throws InterruptedException, TimeoutException + { + ClusterMetadata lastSeen = metadata(); + while (!isCommitted(lastSeen)) + { + runOnce(duration); + lastSeen = metadata(); + + if (executor.isTerminated() && !isCommitted(lastSeen)) + throw new Interruptible.TerminateException(); + } + + return lastSeen; + } + + private boolean isCommitted(ClusterMetadata metadata) + { + return metadata.epoch.isEqualOrAfter(waitingFor); + } + } + } + + private static class Sync extends LocalLog + { + private Sync(LogSpec spec) + { + super(spec); + } + + void runOnce(DurationSpec durationSpec) + { + processPendingInternal(); + } + + synchronized void processPending() + { + processPendingInternal(); + } + + public ClusterMetadata awaitAtLeast(Epoch epoch) + { + processPending(); + if (metadata().epoch.isBefore(epoch)) + throw new IllegalStateException(String.format("Could not reach %s after replay. Highest epoch after replay: %s.", epoch, metadata().epoch)); + + return metadata(); + } + + public void close() + { + } + } + + protected void addListeners() + { + listeners.clear(); + changeListeners.clear(); + asyncChangeListeners.clear(); + + addListener(snapshotListener()); + addListener(new InitializationListener()); + addListener(new SchemaListener()); + addListener(new LegacyStateListener()); + addListener(new PlacementsChangeListener()); + addListener(new MetadataSnapshotListener()); + addListener(new ClientNotificationListener()); + addListener(new UpgradeMigrationListener()); + } + + public void ready() + { + if (!replayComplete.compareAndSet(false, true)) + throw new IllegalStateException("Log is already fully initialised"); + + logger.debug("Marking LocalLog ready at epoch {}", committed.get().epoch); + if (spec.defaultListeners) + { + logger.debug("Adding default listeners to LocalLog"); + addListeners(); + } + else + { + logger.debug("Adding specified listeners to LocalLog"); + spec.listeners.forEach(this::addListener); + spec.changeListeners.forEach(this::addListener); + spec.asyncChangeListeners.forEach(this::addListener); + } + + switch (spec.whenReady) + { + case ALL: + logger.debug("Notifying all registered listeners of both pre and post commit event"); + notifyListeners(spec.initial); + break; + case PRE_COMMIT_ONLY: + logger.debug("Notifying all registered listeners of pre-commit event only"); + notifyPreCommit(spec.initial, committed.get(), true); + break; + case POST_COMMIT_ONLY: + logger.debug("Notifying all registered listeners of post-commit event only"); + notifyPostCommit(spec.initial, committed.get(), true); + break; + case NONE: + logger.debug("Not notifying registered listeners of pre or post commit events"); + break; + } + } + + private LogListener snapshotListener() + { + return (entry, metadata) -> { + if (ClusterMetadataService.state() != ClusterMetadataService.State.LOCAL) + return; + + if ((entry.epoch.getEpoch() % DatabaseDescriptor.getMetadataSnapshotFrequency()) == 0) + { + List list = new ArrayList<>(ClusterMetadata.current().fullCMSMembers()); + list.sort(comparing(i -> i.addressBytes[i.addressBytes.length - 1])); + if (list.get(0).equals(FBUtilities.getBroadcastAddressAndPort())) + ScheduledExecutors.nonPeriodicTasks.submit(() -> ClusterMetadataService.instance().sealPeriod()); + } + }; + } + + private static class StopProcessingException extends RuntimeException + { + private StopProcessingException() + { + super(); + } + + private StopProcessingException(Throwable cause) + { + super(cause); + } + } +} \ No newline at end of file diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeaving.java b/src/java/org/apache/cassandra/tcm/log/LogReader.java similarity index 68% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeaving.java rename to src/java/org/apache/cassandra/tcm/log/LogReader.java index c35d0b4267..72d1a6a04a 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetLeaving.java +++ b/src/java/org/apache/cassandra/tcm/log/LogReader.java @@ -16,14 +16,18 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tcm.log; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingLeavingRunner; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Sealed; -class OnInstanceSetLeaving extends ClusterReliableAction +public interface LogReader { - OnInstanceSetLeaving(ClusterActions actions, int on) + default Replication getReplication(Epoch since) { - super("Set " + on + " to Leaving", actions, on, addToRingLeavingRunner(actions.cluster.get(on))); + Sealed sealed = Sealed.lookupForReplication(since); + return getReplication(sealed.period, since); } + + Replication getReplication(long startPeriod, Epoch since); } diff --git a/src/java/org/apache/cassandra/tcm/log/LogState.java b/src/java/org/apache/cassandra/tcm/log/LogState.java new file mode 100644 index 0000000000..4518659e69 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/LogState.java @@ -0,0 +1,267 @@ +/* + * 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.tcm.log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.JVMStabilityInspector; + +public class LogState +{ + private static final Logger logger = LoggerFactory.getLogger(LogState.class); + public static LogState EMPTY = new LogState(null, Replication.EMPTY); + public static final IVersionedSerializer defaultMessageSerializer = new Serializer(NodeVersion.CURRENT.serializationVersion()); + + private static volatile Serializer serializerCache; + public static IVersionedSerializer messageSerializer(Version version) + { + Serializer cached = serializerCache; + if (cached != null && cached.serializationVersion.equals(version)) + return cached; + cached = new Serializer(version); + serializerCache = cached; + return cached; + } + + public final ClusterMetadata baseState; + public final Replication transformations; + + // Uses Replication rather than an just a list of entries primarily to avoid duplicating the existing serializer + public LogState(ClusterMetadata baseState, Replication transformations) + { + this.baseState = baseState; + this.transformations = transformations; + } + + public static LogState make(ClusterMetadata baseState, Replication increments) + { + return new LogState(baseState, increments); + } + + public Epoch latestEpoch() + { + if (transformations.isEmpty()) + { + if (baseState == null) + return Epoch.EMPTY; + return baseState.epoch; + } + return transformations.latestEpoch(); + } + + public static LogState make(Replication increments) + { + return new LogState(null, increments); + } + + public static LogState make(ClusterMetadata baseState) + { + return new LogState(baseState, Replication.EMPTY); + } + + public boolean isEmpty() + { + return baseState == null && transformations.isEmpty(); + } + + @Override + public String toString() + { + return "LogState{" + + "baseState=" + (baseState != null ? baseState.epoch.toString() : "none ") + + ", transformations=" + transformations.toString() + + '}'; + } + + /** + * Contains the logic for generating a LogState from an arbitrary epoch to the current epoch. + * The LogState returned is suitable for bringing a metadata log up to date with the current state as viewed from + * this local log. This method will attempt to minimise the number of individual log entries contained in the + * LogState. If any snapshots with a higher epoch than the supplied start point, the LogState will include the most + * recent along with any subsequent log entries. + * Callers supply: + * * The epoch to act as the starting point for the LogState. If no metadata snapshot for a higher epoch + * exists, the LogState will contain all log entries with an epoch greater than this. If such a snapshot + * is found, it will form the baseState of the LogState, and only log entries with epochs greater than the + * snapshot's will be included. + * * MetadataSnapshots to provide access to serialized metadata snapshots. Outside of tests, the only + * implementation of this is SystemKeyspaceMetadataSnapshots which persists the info in local system tables. + * Both the metadata_last_sealed_period and metadata_snapshot tables are populated by the after commit hook of a + * SealPeriod transform, so are neither atomically updated nor guaranteed to completely up to date. This is ok, + * though as this method can handle both being missing or out of date by degrading to a slower lookup. + * * A LogReader to return a list of log entries in the form of a Replication given a starting epoch and + * optionally a period if one can be derived from a snapshot. If no snapshot is available, the LogReader + * should determine the starting period itself (typically by checking the system.sealed_periods table, but + * falling back to a full log scan in the pathological case) and will read log entries from either the local or + * distributed log table depending on the calling context. + * @param since + * @param snapshots + * @param reader + * @return + */ + @VisibleForTesting + public static LogState getLogState(Epoch since, MetadataSnapshots snapshots, LogReader reader) + { + try + { + ClusterMetadata snapshot = snapshots.getLatestSnapshotAfter(since); + if (snapshot == null) + { + logger.info("No suitable metadata snapshot found, not including snapshot in LogState since {}", since); + return new LogState(null, reader.getReplication(since)); + } + else + { + logger.info("Loaded snapshot of epoch {} from sealed period {} which follows requested start epoch, including in LogState since {}", + snapshot.epoch, snapshot.period, since.getEpoch()); + return new LogState(snapshot, reader.getReplication(snapshot.nextPeriod(), snapshot.epoch)); + } + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + logger.error("Could not restore the state.", t); + throw new RuntimeException(t); + } + } + + /** + * Contains the logic for generating a LogState up to an arbitrary epoch. + * The LogState returned is suitable for point in time recovery of cluster metadata. If any snapshots are available + * which precede the target epoch, the LogState will include the one with the highest epoch along with subsequent + * entries up to and including the target epoch. + * Callers supply: + * * The epoch to act as the termination point for the LogState. If no metadata snapshot for a lower epoch + * exists, the LogState will contain all log entries with an epoch less than this. If such a snapshot + * is found, it will form the baseState of the LogState, and only log entries with epochs greater than the + * snapshot's and less than or equal to the target will be included. + * * MetadataSnapshots to provide access to serialized metadata snapshots. Outside of tests, the only + * implementation of this is SystemKeyspaceMetadataSnapshots which persists the info in local system tables. + * Both the metadata_last_sealed_period and metadata_snapshot tables are populated by the after commit hook of a + * SealPeriod transform, so are neither atomically updated nor guaranteed to completely up to date. This is ok, + * though as this method can handle both being missing or out of date by degrading to a slower lookup. + * * A LogReader to return a list of log entries in the form of a Replication given a starting epoch and + * optionally a period if one can be derived from a snapshot. If no snapshot is available, the LogReader + * should determine the starting period itself (typically by checking the system.sealed_periods table, but + * falling back to a full log scan in the pathological case) and will read log entries from either the local or + * distributed log table depending on the calling context. + * @param since + * @param snapshots + * @param reader + * @return + */ + public static LogState getForRecovery(Epoch target) + { + LogStorage logStorage = LogStorage.SystemKeyspace; + MetadataSnapshots snapshots = new MetadataSnapshots.SystemKeyspaceMetadataSnapshots(); + Sealed sealed = Sealed.lookupForReplication(target); + // a snapshot exists for exactly the epoch we're looking for + if (sealed.epoch.is(target)) + return LogState.make(snapshots.getSnapshot(sealed.epoch)); + + Sealed preceding; + ClusterMetadata base; + if (sealed.epoch.isAfter(target)) + { + // we need the snapshot from the preceding period plus some entries. Scan result includes the supplied + // start period so we have to either manually decrement the start period (or fetch a list of up to 2 items) + List before = Period.scanLogForRecentlySealed(SystemKeyspace.LocalMetadataLog, sealed.period - 1, 1); + assert !before.isEmpty() : "No earlier snapshot found, started looking at " + (sealed.period - 1) + " target = " + target; + preceding = before.get(0); + base = snapshots.getSnapshot(preceding.epoch); + } + else + { + // scan from the start of the log table - expensive + preceding = new Sealed(Period.FIRST, Epoch.EMPTY); + base = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + } + + // TODO add LogStorage.getReplication(startPeriod, startEpoch, endEpoch); so we don't have to overfetch + Replication allSince = logStorage.getReplication(preceding.period, preceding.epoch); + List entries = new ArrayList<>(); + for (Entry e : allSince.entries()) + { + if (e.epoch.isAfter(target)) + break; + entries.add(e); + } + return LogState.make(base, Replication.of(entries)); + } + + static final class Serializer implements IVersionedSerializer + { + private final Version serializationVersion; + + public Serializer(Version serializationVersion) + { + this.serializationVersion = serializationVersion; + } + + @Override + public void serialize(LogState t, DataOutputPlus out, int version) throws IOException + { + out.writeBoolean(t.baseState != null); + if (t.baseState != null) + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, t.baseState, out, serializationVersion); + VerboseMetadataSerializer.serialize(Replication.serializer, t.transformations, out, serializationVersion); + } + + @Override + public LogState deserialize(DataInputPlus in, int version) throws IOException + { + boolean hasSnapshot = in.readBoolean(); + ClusterMetadata snapshot = null; + if (hasSnapshot) + snapshot = VerboseMetadataSerializer.deserialize(ClusterMetadata.serializer, in); + Replication replication = VerboseMetadataSerializer.deserialize(Replication.serializer, in); + return new LogState(snapshot, replication); + } + + @Override + public long serializedSize(LogState t, int version) + { + long size = TypeSizes.BOOL_SIZE; + if (t.baseState != null) + size += VerboseMetadataSerializer.serializedSize(ClusterMetadata.serializer, t.baseState, serializationVersion); + size += VerboseMetadataSerializer.serializedSize(Replication.serializer, t.transformations, serializationVersion); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/log/LogStorage.java b/src/java/org/apache/cassandra/tcm/log/LogStorage.java new file mode 100644 index 0000000000..e35dcfb2b1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/LogStorage.java @@ -0,0 +1,55 @@ +/* + * 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.tcm.log; + +import org.apache.cassandra.tcm.Epoch; + +public interface LogStorage extends LogReader +{ + void append(long period, Entry entry); + LogState getLogState(Epoch since); + + /** + * We are using system keyspace even on CMS nodes (at least for now) since otherwise it is tricky + * to implement replay from the log, given the distributed metadata log table is created not + * during startup, but is initialized as a distributed table. We can, in theory, add an ability + * to have distributed _system_ tables to avoid this duplication, but this doesn't seem to be + * a priority now, especially given there's nothing that prevents us from truncating the log + * table up to the last snapshot at any given time. + */ + LogStorage SystemKeyspace = new SystemKeyspaceStorage(); + LogStorage None = new NoOpLogStorage(); + + class NoOpLogStorage implements LogStorage + { + public void append(long period, Entry entry) {} + public LogState getLogState(Epoch since) + { + return LogState.EMPTY; + } + public Replication getReplication(Epoch since) + { + return Replication.EMPTY; + } + public Replication getReplication(long startPeriod, Epoch since) + { + return Replication.EMPTY; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/log/Replication.java b/src/java/org/apache/cassandra/tcm/log/Replication.java new file mode 100644 index 0000000000..9db9d8476b --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/Replication.java @@ -0,0 +1,260 @@ +/* + * 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.tcm.log; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class Replication +{ + private static final Logger logger = LoggerFactory.getLogger(Replication.class); + public static Replication EMPTY = new Replication(ImmutableList.builder().build()); + + public static final Serializer serializer = new Serializer(); + public static final IVersionedSerializer defaultMessageSerializer = new MessageSerializer(NodeVersion.CURRENT.serializationVersion()); + + private static volatile MessageSerializer serializerCache; + public static IVersionedSerializer messageSerializer(Version version) + { + MessageSerializer cached = serializerCache; + if (cached != null && cached.serializationVersion.equals(version)) + return cached; + cached = new MessageSerializer(version); + serializerCache = cached; + return cached; + } + + + private final ImmutableList entries; + + public Replication(Collection entries) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Entry entry : entries) + builder.add(entry); + this.entries = builder.build(); + } + + public static Replication of(Entry entry) + { + return new Replication(Collections.singletonList(entry)); + } + + public static Replication of(Collection entries) + { + return new Replication(ImmutableList.copyOf(entries)); + } + + public Replication(ImmutableList entries) + { + this.entries = entries; + } + + public ImmutableList entries() + { + return entries; + } + + public Replication retainFrom(Epoch epoch) + { + ImmutableList.Builder builder = ImmutableList.builder(); + entries.stream().filter(entry -> entry.epoch.isEqualOrAfter(epoch)).forEach(builder::add); + return new Replication(builder.build()); + } + + public Epoch latestEpoch() + { + return tail().epoch; + } + + private Entry tail() + { + // TODO possible empty list + return entries.get(entries.size() - 1); + } + + public boolean isEmpty() + { + return entries.isEmpty(); + } + + public Epoch apply(LocalLog log) + { + log.append(entries()); + return log.waitForHighestConsecutive().epoch; + } + + @Override + public String toString() + { + return "Replication{" + + "size=" + entries.size() + + (entries.isEmpty() ? "" + : ", min=" + entries.get(0).epoch + + ", max=" + entries.get(entries.size() - 1).epoch) + + '}'; + } + + public static final class Serializer implements MetadataSerializer + { + @Override + public void serialize(Replication t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.entries.size()); + for (Entry entry : t.entries) + Entry.serializer.serialize(entry, out, version); + } + + @Override + public Replication deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + ImmutableList.Builder builder = ImmutableList.builder(); + for(int i=0;i + { + private final Version serializationVersion; + + public MessageSerializer(Version serializationVersion) + { + this.serializationVersion = serializationVersion; + } + + @Override + public void serialize(Replication t, DataOutputPlus out, int version) throws IOException + { + VerboseMetadataSerializer.serialize(serializer, t, out, serializationVersion); + } + + @Override + public Replication deserialize(DataInputPlus in, int version) throws IOException + { + return VerboseMetadataSerializer.deserialize(serializer, in); + } + + @Override + public long serializedSize(Replication t, int version) + { + return VerboseMetadataSerializer.serializedSize(serializer, t, serializationVersion); + } + } + + public static final class ReplicationHandler implements IVerbHandler + { + private static final Logger logger = LoggerFactory.getLogger(ReplicationHandler.class); + private final LocalLog log; + + public ReplicationHandler(LocalLog log) + { + this.log = log; + } + + public void doVerb(Message message) throws IOException + { + logger.info("Received log replication {} from {}", message.payload, message.from()); + log.append(message.payload.entries); + } + } + + /** + * Log Notification handler is similar to regular replication handler, except that + * notifying side actually expects the response from the replica, and we need to be fully + * caught up to the latest epoch in the replication, since replicas should be + * able to enact the latest epoch as soon as it is watermarked by CMS. + */ + public static class LogNotifyHandler implements IVerbHandler + { + private final LocalLog log; + public LogNotifyHandler(LocalLog log) + { + this.log = log; + } + + public void doVerb(Message message) throws IOException + { + // If another node (CMS or otherwise) is sending log notifications then + // we can infer that the post-upgrade enablement of CMS has completed + if (ClusterMetadataService.instance().isMigrating()) + { + logger.info("Received metadata log notification from {}, marking in progress migration complete", message.from()); + ClusterMetadataService.instance().migrated(); + } + + log.append(message.payload); + if (log.hasGaps()) + { + Optional highestPending = log.highestPending(); + if (highestPending.isPresent()) + { + // We should not call maybeCatchup fom this stage + ScheduledExecutors.optionalTasks.submit(() -> ClusterMetadataService.instance().fetchLogFromCMS(highestPending.get())); + } + else if (ClusterMetadata.current().epoch.isBefore(message.payload.transformations.latestEpoch())) + { + throw new IllegalStateException(String.format("Should have caught up to at least %s, but got only %s", + message.payload.transformations.latestEpoch(), ClusterMetadata.current().epoch)); + } + } + else + log.waitForHighestConsecutive(); + + Message response = message.responseWith(ClusterMetadata.current().epoch); + MessagingService.instance().send(response, message.from()); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/log/SystemKeyspaceStorage.java b/src/java/org/apache/cassandra/tcm/log/SystemKeyspaceStorage.java new file mode 100644 index 0000000000..83c443731b --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/log/SystemKeyspaceStorage.java @@ -0,0 +1,162 @@ +/* + * 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.tcm.log; + +import java.nio.ByteBuffer; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Transformation; + +import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; +import static org.apache.cassandra.db.SystemKeyspace.LocalMetadataLog; + +public class SystemKeyspaceStorage implements LogStorage +{ + private static final Logger logger = LoggerFactory.getLogger(SystemKeyspaceStorage.class); + public static final String NAME = org.apache.cassandra.db.SystemKeyspace.METADATA_LOG; + + /** + * Generation is used as a timestamp for automatic table creation on startup. + * If you make any changes to the tables below, make sure to increment the + * generation and document your change here. + *

    + * gen 0: original definition in 5.0 + */ + public static final long GENERATION = 0; + + private final Supplier snapshots; + + public SystemKeyspaceStorage() + { + this(() -> ClusterMetadataService.instance().snapshotManager()); + } + + @VisibleForTesting + public SystemKeyspaceStorage(Supplier snapshots) + { + this.snapshots = snapshots; + } + + // This method is always called from a single thread, so doesn't have to be synchonised. + public void append(long period, Entry entry) + { + try + { + // TODO get lowest supported metadata version from ClusterMetadata + ByteBuffer serializedTransformation = entry.transform.kind().toVersionedBytes(entry.transform); + String query = String.format("INSERT INTO %s.%s (period, epoch, current_epoch, entry_id, transformation, kind) VALUES (?,?,?,?,?,?)", + SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME); + executeInternal(query, period, entry.epoch.getEpoch(), entry.epoch.getEpoch(), + entry.id.entryId, serializedTransformation, entry.transform.kind().toString()); + // todo; should probably not flush every time, but it simplifies tests + Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(NAME).forceBlockingFlush(ColumnFamilyStore.FlushReason.INTERNALLY_FORCED); + } + catch (Throwable t) + { + logger.error("Could not persist the entry {} proceeding with in-memory commit.", entry, t); + } + } + + public synchronized static boolean hasAnyEpoch() + { + String query = String.format("SELECT epoch FROM %s.%s LIMIT 1", SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME); + + for (UntypedResultSet.Row row : executeInternal(query)) + return true; + + return false; + } + + public LogState getLogState(Epoch since) + { + return LogState.getLogState(since, snapshots.get(), this); + } + + /** + * Uses the supplied period as a starting point to iterate through the log table + * collating log entries which follow the supplied epoch. It is assumed that the + * target epoch is found in the starting period, so any entries returned will be + * from either the starting period or subsequent periods. + * @param since target epoch + * @return contiguous list of log entries which follow the given epoch, + * which may be empty + * @param startPeriod + * @param since + * @return + */ + public Replication getReplication(long startPeriod, Epoch since) + { + try + { + if (startPeriod == Period.EMPTY) + { + startPeriod = Period.scanLogForPeriod(LocalMetadataLog, since); + if (startPeriod == Period.EMPTY) + return Replication.EMPTY; + } + + long period = startPeriod; + ImmutableList.Builder entries = new ImmutableList.Builder<>(); + while (true) + { + boolean empty = true; + + UntypedResultSet resultSet = executeInternal(String.format("SELECT epoch, kind, transformation, entry_id FROM %s.%s WHERE period = ? and epoch > ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME), + period, since.getEpoch()); + + for (UntypedResultSet.Row row : resultSet) + { + long entryId = row.getLong("entry_id"); + Epoch epoch = Epoch.create(row.getLong("epoch")); + ByteBuffer transformationBlob = row.getBlob("transformation"); + Transformation.Kind kind = Transformation.Kind.valueOf(row.getString("kind")); + Transformation transform = kind.fromVersionedBytes(transformationBlob); + kind.fromVersionedBytes(transformationBlob); + entries.add(new Entry(new Entry.Id(entryId), epoch, transform)); + empty = false; + } + + if (period != startPeriod && empty) + break; + + period++; + } + + return new Replication(entries.build()); + } + catch (Exception e) + { + logger.error("Could not restore the state.", e); + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/membership/Directory.java b/src/java/org/apache/cassandra/tcm/membership/Directory.java new file mode 100644 index 0000000000..ca5beecda5 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/Directory.java @@ -0,0 +1,757 @@ +/* + * 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.tcm.membership; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.UUIDSerializer; +import org.apache.cassandra.utils.btree.BTreeBiMap; +import org.apache.cassandra.utils.btree.BTreeMap; +import org.apache.cassandra.utils.btree.BTreeMultimap; + +import static org.apache.cassandra.db.TypeSizes.sizeof; +import static org.apache.cassandra.tcm.membership.NodeVersion.CURRENT; + +public class Directory implements MetadataValue +{ + public static final Serializer serializer = new Serializer(); + + public static Directory EMPTY = new Directory(); + + private final int nextId; + private final Epoch lastModified; + private final BTreeBiMap peers; + private final BTreeMap locations; + public final BTreeMap states; + public final BTreeMap versions; + public final BTreeMap addresses; + private final BTreeBiMap hostIds; + private final BTreeMultimap endpointsByDC; + private final BTreeMap> racksByDC; + public final NodeVersion clusterMinVersion; + public final NodeVersion clusterMaxVersion; + + public Directory() + { + this(1, + Epoch.EMPTY, + BTreeBiMap.empty(), + BTreeMap.empty(), + BTreeMap.empty(), + BTreeMap.empty(), + BTreeBiMap.empty(), + BTreeMap.empty(), + BTreeMultimap.empty(), + BTreeMap.empty()); + } + + private Directory(int nextId, + Epoch lastModified, + BTreeBiMap peers, + BTreeMap locations, + BTreeMap states, + BTreeMap versions, + BTreeBiMap hostIds, + BTreeMap addresses, + BTreeMultimap endpointsByDC, + BTreeMap> racksByDC) + { + this.nextId = nextId; + this.lastModified = lastModified; + this.peers = peers; + this.locations = locations; + this.states = states; + this.versions = versions; + this.hostIds = hostIds; + this.addresses = addresses; + this.endpointsByDC = endpointsByDC; + this.racksByDC = racksByDC; + Pair minMaxVer = minMaxVersions(states, versions); + clusterMinVersion = minMaxVer.left; + clusterMaxVersion = minMaxVer.right; + } + + @Override + public String toString() + { + return "Directory{" + + "nextId=" + nextId + + ", lastModified=" + lastModified + + ", peers=" + peers + + ", locations=" + locations + + ", states=" + states + + ", versions=" + versions + + ", addresses=" + addresses + + ", hostIds=" + hostIds + + ", endpointsByDC=" + endpointsByDC + + ", racksByDC=" + racksByDC + + '}'; + } + + public Set toNodeIds(Collection addrs) + { + Set nodeIds = new HashSet<>(); + for (InetAddressAndPort addr : addrs) + nodeIds.add(peerId(addr)); + return nodeIds; + } + + @Override + public Epoch lastModified() + { + return lastModified; + } + + @Override + public Directory withLastModified(Epoch epoch) + { + return new Directory(nextId, epoch, peers, locations, states, versions, hostIds, addresses, endpointsByDC, racksByDC); + } + + public Directory withNonUpgradedNode(NodeAddresses addresses, + Location location, + NodeVersion version, + NodeState state, + UUID hostId) + { + NodeId id = new NodeId(nextId); + return with(addresses, id, hostId, location, version).withNodeState(id, state).withRackAndDC(id); + } + + @VisibleForTesting + public Directory with(NodeAddresses addresses, Location location) + { + return with(addresses, location, CURRENT); + } + + public Directory with(NodeAddresses addresses, Location location, NodeVersion nodeVersion) + { + NodeId id = new NodeId(nextId); + if (peers.containsKey(id)) + throw new IllegalStateException("Directory already contains a node with id " + id); + return with(addresses, id, id.toUUID(), location, nodeVersion); + } + + private Directory with(NodeAddresses nodeAddresses, NodeId id, UUID hostId, Location location, NodeVersion nodeVersion) + { + if (peers.containsKey(id)) + return this; + if (peers.containsValue(nodeAddresses.broadcastAddress)) + return this; + if (locations.containsKey(id)) + return this; + + return new Directory(nextId + 1, + lastModified, + peers.without(id).with(id, nodeAddresses.broadcastAddress), + locations.withForce(id, location), + states.withForce(id, NodeState.REGISTERED), + versions.withForce(id, nodeVersion), + hostIds.withForce(id, hostId), + addresses.withForce(id, nodeAddresses), + endpointsByDC, + racksByDC); + } + + public Directory withNodeState(NodeId id, NodeState state) + { + return new Directory(nextId, lastModified, peers, locations, states.withForce(id, state), versions, hostIds, addresses, endpointsByDC, racksByDC); + } + + public Directory withNodeVersion(NodeId id, NodeVersion version) + { + if (Objects.equals(versions.get(id), version)) + return this; + return new Directory(nextId, lastModified, peers, locations, states, versions.withForce(id, version), hostIds, addresses, endpointsByDC, racksByDC); + } + + public Directory withNodeAddresses(NodeId id, NodeAddresses nodeAddresses) + { + if (Objects.equals(addresses.get(id), nodeAddresses)) + return this; + + InetAddressAndPort oldEp = addresses.get(id).broadcastAddress; + BTreeMultimap updatedEndpointsByDC = endpointsByDC.without(location(id).datacenter, oldEp) + .with(location(id).datacenter, nodeAddresses.broadcastAddress); + + Location location = location(id); + BTreeMultimap rackEP = (BTreeMultimap) racksByDC.get(location.datacenter); + if (rackEP == null) + rackEP = BTreeMultimap.empty(); + + rackEP = rackEP.without(location.rack, oldEp) + .with(location.rack, nodeAddresses.broadcastAddress); + BTreeMap> updatedEndpointsByRack = racksByDC.withForce(location(id).datacenter, rackEP); + + return new Directory(nextId, lastModified, + peers.withForce(id,nodeAddresses.broadcastAddress), locations, states, versions, hostIds, addresses.withForce(id, nodeAddresses), + updatedEndpointsByDC, + updatedEndpointsByRack); + } + + public Directory withRackAndDC(NodeId id) + { + InetAddressAndPort endpoint = peers.get(id); + Location location = locations.get(id); + + BTreeMultimap rackEP = (BTreeMultimap) racksByDC.get(location.datacenter); + if (rackEP == null) + rackEP = BTreeMultimap.empty(); + rackEP = rackEP.with(location.rack, endpoint); + + return new Directory(nextId, lastModified, peers, locations, states, versions, hostIds, addresses, + endpointsByDC.with(location.datacenter, endpoint), + racksByDC.withForce(location.datacenter, rackEP)); + } + + public Directory withoutRackAndDC(NodeId id) + { + InetAddressAndPort endpoint = peers.get(id); + Location location = locations.get(id); + BTreeMultimap rackEP = (BTreeMultimap) racksByDC.get(location.datacenter); + rackEP = rackEP.without(location.rack, endpoint); + BTreeMap> newRacksByDC; + if (rackEP.isEmpty()) + newRacksByDC = racksByDC.without(location.datacenter); + else + newRacksByDC = racksByDC.withForce(location.datacenter, rackEP); + return new Directory(nextId, lastModified, peers, locations, states, versions, hostIds, addresses, + endpointsByDC.without(location.datacenter, endpoint), + newRacksByDC); + } + + public Directory without(NodeId id) + { + InetAddressAndPort endpoint = peers.get(id); + Location location = locations.get(id); + // Last node in dc + if (!racksByDC.containsKey(location.datacenter)) + { + assert !endpointsByDC.containsKey(location.datacenter); + + return new Directory(nextId, + lastModified, + peers.without(id), + locations.without(id), + states.without(id), + versions.without(id), + hostIds.without(id), + addresses.without(id), + endpointsByDC, + racksByDC); + + } + + BTreeMultimap rackEP = (BTreeMultimap) racksByDC.get(location.datacenter); + rackEP = rackEP.without(location.rack, endpoint); + + return new Directory(nextId, + lastModified, + peers.without(id), + locations.without(id), + states.without(id), + versions.without(id), + hostIds.without(id), + addresses.without(id), + endpointsByDC.without(location.datacenter, endpoint), + racksByDC.withForce(location.datacenter, rackEP)); + } + + public NodeId peerId(InetAddressAndPort endpoint) + { + return peers.inverse().get(endpoint); + } + + public boolean isRegistered(InetAddressAndPort endpoint) + { + return peers.inverse().containsKey(endpoint); + } + + public InetAddressAndPort endpoint(NodeId id) + { + return peers.get(id); + } + + public boolean isEmpty() + { + return peers.isEmpty(); + } + + /** + * Includes every registered endpoint, including those which haven't yet joined and those which have + * left but are yet to be unregistered. Not for use when calculating availablity or placements, in + * those cases use allJoinedEndpoints. + * @return + */ + public ImmutableList allAddresses() + { + return ImmutableList.copyOf(peers.values()); + } + + public ImmutableSet peerIds() + { + return ImmutableSet.copyOf(peers.keySet()); + } + + public NodeAddresses getNodeAddresses(NodeId id) + { + return addresses.get(id); + } + + private Node getNode(NodeId id) + { + return new Node(id, addresses.get(id), locations.get(id), states.get(id), versions.get(id), hostIds.get(id)); + } + + public Location location(NodeId id) + { + return locations.get(id); + } + + public Set datacenterEndpoints(String datacenter) + { + return (Set) endpointsByDC.get(datacenter); + } + + public Multimap datacenterRacks(String datacenter) + { + return racksByDC.get(datacenter); + } + + public NodeState peerState(NodeId peer) + { + return states.get(peer); + } + + public NodeVersion version(NodeId peer) + { + return versions.get(peer); + } + + public UUID hostId(NodeId peer) + { + return hostIds.getOrDefault(peer, peer.toUUID()); + } + + /** + * Retrieve the NodeId for a peer from its pre-upgrade HostId + * @param hostId + * @return NodeId for the peer which prior to upgrade had the supplied Host ID, or null if no mapping is found + */ + @Nullable + public NodeId nodeIdFromHostId(UUID hostId) + { + return hostIds.inverse().getOrDefault(hostId, null); + } + + public Map> allDatacenterRacks() + { + return racksByDC; + } + + public Set knownDatacenters() + { + return locations.values().stream().map(l -> l.datacenter).collect(Collectors.toSet()); + } + + public Multimap allDatacenterEndpoints() + { + return endpointsByDC; + } + + public Collection allJoinedEndpoints() + { + return endpointsByDC.values(); + } + + public NodeState peerState(InetAddressAndPort peer) + { + return states.get(peers.inverse().get(peer)); + } + + public String toDebugString() + { + return peers.keySet() + .stream() + .sorted() + .map(this::getNode) + .map(Node::toString) + .collect(Collectors.joining("\n")); + } + + private static class Node + { + public static final Serializer serializer = new Serializer(); + + public final NodeId id; + public final NodeAddresses addresses; + public final Location location; + public final NodeState state; + public final NodeVersion version; + public final UUID hostId; + + public Node(NodeId id, NodeAddresses addresses, Location location, NodeState state, NodeVersion version, UUID hostId) + { + this.id = Preconditions.checkNotNull(id, "Node ID must not be null"); + this.addresses = Preconditions.checkNotNull(addresses, "Node addresses must not be null"); + this.location = Preconditions.checkNotNull(location, "Node location must not be null"); + this.state = Preconditions.checkNotNull(state, "Node state must not be null"); + this.version = Preconditions.checkNotNull(version, "Node version must not be null"); + this.hostId = hostId; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Node node = (Node) o; + return id.equals(node.id) + && addresses.equals(node.addresses) + && location.equals(node.location) + && state == node.state + && version.equals(node.version); + } + + + @Override + public int hashCode() + { + return Objects.hash(id, addresses, location, state, version); + } + + @Override + public String toString() + { + return "Node{" + + "id=" + id + + ", addresses=" + addresses + + ", location=" + location + + ", state=" + state + + ", version=" + version + + '}'; + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Node node, DataOutputPlus out, Version version) throws IOException + { + NodeId.serializer.serialize(node.id, out, version); + NodeAddresses.serializer.serialize(node.addresses, out, version); + out.writeUTF(node.location.datacenter); + out.writeUTF(node.location.rack); + out.writeInt(node.state.ordinal()); + NodeVersion.serializer.serialize(node.version, out, version); + if (node.hostId == null) + out.writeBoolean(false); + else + { + out.writeBoolean(true); + UUIDSerializer.serializer.serialize(node.hostId, out, MessagingService.VERSION_50); + } + + } + + public Node deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId id = NodeId.serializer.deserialize(in, version); + NodeAddresses addresses = NodeAddresses.serializer.deserialize(in, version); + Location location = new Location(in.readUTF(), in.readUTF()); + NodeState state = NodeState.values()[in.readInt()]; + NodeVersion nodeVersion = NodeVersion.serializer.deserialize(in, version); + boolean hasHostId = in.readBoolean(); + UUID hostId = hasHostId ? UUIDSerializer.serializer.deserialize(in, MessagingService.VERSION_50) : null; + return new Node(id, addresses, location, state, nodeVersion, hostId); + } + + public long serializedSize(Node node, Version version) + { + long size = 0; + size += NodeId.serializer.serializedSize(node.id, version); + size += NodeAddresses.serializer.serializedSize(node.addresses, version); + size += sizeof(node.location.datacenter); + size += sizeof(node.location.rack); + size += TypeSizes.INT_SIZE; + size += NodeVersion.serializer.serializedSize(node.version, version); + size += TypeSizes.BOOL_SIZE; + if (node.hostId != null) + size += UUIDSerializer.serializer.serializedSize(node.hostId, MessagingService.VERSION_50); + return size; + } + } + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Directory t, DataOutputPlus out, Version version) throws IOException + { + if (version.isAtLeast(Version.V1)) + out.writeInt(t.nextId); + out.writeInt(t.states.size()); + for (NodeId nodeId : t.states.keySet()) + Node.serializer.serialize(t.getNode(nodeId), out, version); + + Set dcs = t.racksByDC.keySet(); + out.writeInt(dcs.size()); + for (String dc : dcs) + { + out.writeUTF(dc); + Map> racks = t.racksByDC.get(dc).asMap(); + out.writeInt(racks.size()); + for (String rack : racks.keySet()) + { + out.writeUTF(rack); + Collection endpoints = racks.get(rack); + out.writeInt(endpoints.size()); + for (InetAddressAndPort endpoint : endpoints) + { + InetAddressAndPort.MetadataSerializer.serializer.serialize(endpoint, out, version); + } + } + } + Epoch.serializer.serialize(t.lastModified, out, version); + } + + public Directory deserialize(DataInputPlus in, Version version) throws IOException + { + int nextId = -1; + if (version.isAtLeast(Version.V1)) + nextId = in.readInt(); + int count = in.readInt(); + Directory newDir = new Directory(); + + for (int i = 0; i < count; i++) + { + Node n = Node.serializer.deserialize(in, version); + // todo: bulk operations + newDir = newDir.with(n.addresses, n.id, n.hostId, n.location, n.version) + .withNodeState(n.id, n.state); + } + + int dcCount = in.readInt(); + BTreeMultimap dcEndpoints = BTreeMultimap.empty(); + BTreeMap> racksByDC = BTreeMap.empty(); + for (int i=0; i rackEndpoints = BTreeMultimap.empty(); + for (int j=0; j 0) + maxId = id; + } + + if (maxId == null) + nextId = 1; + else + nextId = maxId.id() + 1; + } + return new Directory(nextId, + lastModified, + newDir.peers, + newDir.locations, + newDir.states, + newDir.versions, + newDir.hostIds, + newDir.addresses, + dcEndpoints, + racksByDC); + } + + public long serializedSize(Directory t, Version version) + { + long size = 0; + if (version.isAtLeast(Version.V1)) + size += sizeof(t.nextId); + + size += sizeof(t.states.size()); + for (NodeId nodeId : t.states.keySet()) + size += Node.serializer.serializedSize(t.getNode(nodeId), version); + + size += sizeof(t.racksByDC.size()); + for (Map.Entry> entry : t.racksByDC.entrySet()) + { + size += sizeof(entry.getKey()); + Map> racks = entry.getValue().asMap(); + size += sizeof(racks.size()); + for (Map.Entry> e : racks.entrySet()) + { + size += sizeof(e.getKey()); + Collection endpoints = e.getValue(); + size += sizeof(endpoints.size()); + for (InetAddressAndPort endpoint : endpoints) + { + size += InetAddressAndPort.MetadataSerializer.serializer.serializedSize(endpoint, version); + } + } + } + size += Epoch.serializer.serializedSize(t.lastModified, version); + return size; + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Directory)) return false; + Directory directory = (Directory) o; + + return Objects.equals(lastModified, directory.lastModified) && + isEquivalent(directory); + } + + private static Pair minMaxVersions(BTreeMap states, BTreeMap versions) + { + NodeVersion minVersion = null; + NodeVersion maxVersion = null; + for (Map.Entry entry : states.entrySet()) + { + if (entry.getValue() != NodeState.LEFT) + { + NodeVersion ver = versions.get(entry.getKey()); + if (minVersion == null || ver.compareTo(minVersion) < 0) + minVersion = ver; + if (maxVersion == null || ver.compareTo(maxVersion) > 0) + maxVersion = ver; + } + } + if (minVersion == null) + return Pair.create(CURRENT, CURRENT); + return Pair.create(minVersion, maxVersion); + } + + @Override + public int hashCode() + { + return Objects.hash(nextId, lastModified, peers, locations, states, endpointsByDC, racksByDC, versions, addresses); + } + + /** + * returns true if this directory is functionally equivalent to the given one + * + * does not check equality of lastModified + */ + @VisibleForTesting + public boolean isEquivalent(Directory directory) + { + return nextId == directory.nextId && + Objects.equals(peers, directory.peers) && + Objects.equals(locations, directory.locations) && + Objects.equals(states, directory.states) && + Objects.equals(endpointsByDC, directory.endpointsByDC) && + Objects.equals(racksByDC, directory.racksByDC) && + Objects.equals(versions, directory.versions) && + Objects.equals(addresses, directory.addresses); + } + + private static final Logger logger = LoggerFactory.getLogger(Directory.class); + + public void dumpDiff(Directory other) + { + if (nextId != other.nextId) + { + logger.warn("nextId differ: {} != {}", nextId, other.nextId); + } + if (!Objects.equals(peers, other.peers)) + { + logger.warn("Peers differ: {} != {}", peers, other.peers); + dumpDiff(logger, peers, other.peers); + } + if (!Objects.equals(states, other.states)) + { + logger.warn("States differ: {} != {}", states, other.states); + dumpDiff(logger, states, other.states); + } + if (!Objects.equals(endpointsByDC, other.endpointsByDC)) + { + logger.warn("Endpoints by dc differ: {} != {}", endpointsByDC, other.endpointsByDC); + dumpDiff(logger, endpointsByDC.asMap(), other.endpointsByDC.asMap()); + } + if (!Objects.equals(versions, other.versions)) + { + logger.warn("Versions differ: {} != {}", versions, other.versions); + dumpDiff(logger, versions, other.versions); + } + if (!Objects.equals(addresses, other.addresses)) + { + logger.warn("Addresses differ: {} != {}", addresses, other.addresses); + dumpDiff(logger, addresses, other.addresses); + } + } + + public static void dumpDiff(Logger logger, Map l, Map r) + { + for (K k : Sets.intersection(l.keySet(), r.keySet())) + { + V lv = l.get(k); + V rv = r.get(k); + if (!Objects.equals(lv, rv)) + logger.warn("Values for key {} differ: {} != {}", k, lv, rv); + } + for (K k : Sets.difference(l.keySet(), r.keySet())) + logger.warn("Value for key {} is only present in the left set: {}", k, l.get(k)); + for (K k : Sets.difference(r.keySet(), l.keySet())) + logger.warn("Value for key {} is only present in the right set: {}", k, r.get(k)); + + } +} diff --git a/src/java/org/apache/cassandra/tcm/membership/Location.java b/src/java/org/apache/cassandra/tcm/membership/Location.java new file mode 100644 index 0000000000..7e38db79cb --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/Location.java @@ -0,0 +1,83 @@ +/* + * 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.tcm.membership; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class Location +{ + public static final Serializer serializer = new Serializer(); + + public final String datacenter; + public final String rack; + + public Location(String datacenter, String rack) + { + this.datacenter = datacenter; + this.rack = rack; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Location location = (Location) o; + return Objects.equals(datacenter, location.datacenter) && Objects.equals(rack, location.rack); + } + + @Override + public int hashCode() + { + return Objects.hash(datacenter, rack); + } + + @Override + public String toString() + { + return datacenter + '/' + rack; + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Location t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.datacenter); + out.writeUTF(t.rack); + } + + public Location deserialize(DataInputPlus in, Version version) throws IOException + { + return new Location(in.readUTF(), in.readUTF()); + } + + public long serializedSize(Location t, Version version) + { + return TypeSizes.sizeof(t.datacenter) + + TypeSizes.sizeof(t.rack); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeAddresses.java b/src/java/org/apache/cassandra/tcm/membership/NodeAddresses.java new file mode 100644 index 0000000000..53f2d1acba --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/NodeAddresses.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.membership; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; + +public class NodeAddresses +{ + public static final Serializer serializer = new Serializer(); + + // Used during registration in order to ensure identity of the submitter + private final UUID identityToken; + + public final InetAddressAndPort broadcastAddress; + public final InetAddressAndPort localAddress; + public final InetAddressAndPort nativeAddress; + + /** + * + * @param broadcastAddress this comes from config if broadcast_address is set or it falls through to getLocalAddress + * which either grabs the local host address or listen_address from config if set + * todo; config broadcast_address can be changed by snitch (EC2MultiRegionSnitch) during runtime, handle that + * @param localAddress this is the local host if listen_address is not set in config + * @param nativeAddress address for clients to communicate with this node + */ + public NodeAddresses(UUID identityToken, InetAddressAndPort broadcastAddress, InetAddressAndPort localAddress, InetAddressAndPort nativeAddress) + { + this.identityToken = identityToken; + this.broadcastAddress = broadcastAddress; + this.localAddress = localAddress; + this.nativeAddress = nativeAddress; + } + + @VisibleForTesting + public NodeAddresses(InetAddressAndPort address) + { + this(UUID.randomUUID(), address, address, address); + } + + @Override + public String toString() + { + return "NodeAddresses{" + + "broadcastAddress=" + broadcastAddress + + ", localAddress=" + localAddress + + ", nativeAddress=" + nativeAddress + + '}'; + } + + public boolean identityMatches(NodeAddresses other) + { + if (other == null) + return false; + return this.identityToken.equals(other.identityToken); + } + + public boolean conflictsWith(NodeAddresses other) + { + return broadcastAddress.equals(other.broadcastAddress) || + localAddress.equals(other.localAddress) || + nativeAddress.equals(other.nativeAddress); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof NodeAddresses)) return false; + NodeAddresses that = (NodeAddresses) o; + return Objects.equals(broadcastAddress, that.broadcastAddress) && Objects.equals(localAddress, that.localAddress) && Objects.equals(nativeAddress, that.nativeAddress); + } + + @Override + public int hashCode() + { + return Objects.hash(broadcastAddress, localAddress, nativeAddress); + } + + public static NodeAddresses current() + { + return new NodeAddresses(UUID.randomUUID(), + FBUtilities.getBroadcastAddressAndPort(), + FBUtilities.getLocalAddressAndPort(), + FBUtilities.getBroadcastNativeAddressAndPort()); + } + + public static class Serializer implements MetadataSerializer + { + @Override + public void serialize(NodeAddresses t, DataOutputPlus out, Version version) throws IOException + { + out.writeLong(t.identityToken.getMostSignificantBits()); + out.writeLong(t.identityToken.getLeastSignificantBits()); + InetAddressAndPort.MetadataSerializer.serializer.serialize(t.broadcastAddress, out, version); + InetAddressAndPort.MetadataSerializer.serializer.serialize(t.localAddress, out, version); + InetAddressAndPort.MetadataSerializer.serializer.serialize(t.nativeAddress, out, version); + } + + @Override + public NodeAddresses deserialize(DataInputPlus in, Version version) throws IOException + { + UUID token = new UUID(in.readLong(), in.readLong()); + InetAddressAndPort broadcastAddress = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + InetAddressAndPort localAddress = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + InetAddressAndPort rpcAddress = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + return new NodeAddresses(token, broadcastAddress, localAddress, rpcAddress); + } + + @Override + public long serializedSize(NodeAddresses t, Version version) + { + return (2 * Long.BYTES) + + InetAddressAndPort.MetadataSerializer.serializer.serializedSize(t.broadcastAddress, version) + + InetAddressAndPort.MetadataSerializer.serializer.serializedSize(t.localAddress, version) + + InetAddressAndPort.MetadataSerializer.serializer.serializedSize(t.nativeAddress, version); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeId.java b/src/java/org/apache/cassandra/tcm/membership/NodeId.java new file mode 100644 index 0000000000..f011314815 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/NodeId.java @@ -0,0 +1,126 @@ +/* + * 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.tcm.membership; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +import com.google.common.primitives.Ints; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class NodeId implements Comparable, MultiStepOperation.SequenceKey +{ + private final static long NODE_ID_UUID_MAGIC = 7861390860069061072L; + public static final Serializer serializer = new Serializer(); + + private final int id; + + public NodeId(int id) + { + this.id = id; + } + + public static NodeId fromString(String nodeOrHostId) + { + if (nodeOrHostId.length() == UUID.randomUUID().toString().length()) + return NodeId.fromUUID(UUID.fromString(nodeOrHostId)); + return new NodeId(Integer.parseInt(nodeOrHostId)); + } + + public static NodeId fromUUID(UUID uuid) + { + if (!isValidNodeId(uuid)) + throw new UnsupportedOperationException("Not a node id: " + uuid); // see RemoveTest#testBadHostId + + long id = 0x0FFFFFFFFFFFFFFFL & uuid.getLeastSignificantBits(); + return new NodeId(Ints.checkedCast(id)); + } + + public static boolean isValidNodeId(UUID uuid) + { + long id = 0x0FFFFFFFFFFFFFFFL & uuid.getLeastSignificantBits(); + return (uuid.getMostSignificantBits() == NODE_ID_UUID_MAGIC && id < Integer.MAX_VALUE) || + (uuid.getMostSignificantBits() == 0 && uuid.getLeastSignificantBits() < Integer.MAX_VALUE); // old check, for existing cluster upgrades, no need upstream + } + + @Deprecated(since = "CEP-21") + public UUID toUUID() + { + long lsb = 0xC000000000000000L | id; + return new UUID(NODE_ID_UUID_MAGIC, lsb); + } + + public int id() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodeId nodeId = (NodeId) o; + return Objects.equals(id, nodeId.id); + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public String toString() + { + return "NodeId{" + + "id=" + id + + '}'; + } + + public int compareTo(NodeId o) + { + return Integer.compare(id, o.id); + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(NodeId n, DataOutputPlus out, Version version) throws IOException + { + out.writeUnsignedVInt32(n.id); + } + + public NodeId deserialize(DataInputPlus in, Version version) throws IOException + { + return new NodeId(in.readUnsignedVInt32()); + } + + public long serializedSize(NodeId t, Version version) + { + return TypeSizes.sizeofUnsignedVInt(t.id); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeState.java b/src/java/org/apache/cassandra/tcm/membership/NodeState.java new file mode 100644 index 0000000000..e7791a5e7c --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/NodeState.java @@ -0,0 +1,46 @@ +/* + * 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.tcm.membership; + +import java.util.EnumSet; +import java.util.Set; + +public enum NodeState +{ + REGISTERED, + BOOTSTRAPPING, + BOOT_REPLACING, + JOINED, + LEAVING, + LEFT, + MOVING; + private static final Set PRE_JOIN_STATES = EnumSet.of(REGISTERED, BOOTSTRAPPING, BOOT_REPLACING); + private static final Set BOOTSTRAP_STATES = EnumSet.of(BOOTSTRAPPING, BOOT_REPLACING); + + public static boolean isPreJoin(NodeState state) + { + return (state == null || PRE_JOIN_STATES.contains(state)); + } + + public static boolean isBootstrap(NodeState state) + { + return (state != null && BOOTSTRAP_STATES.contains(state)); + } + // TODO: probably we can make these states even more nuanced, and track which step each node is on to have a simpler representation of transition states +} diff --git a/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java new file mode 100644 index 0000000000..4428a169e3 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/membership/NodeVersion.java @@ -0,0 +1,137 @@ +/* + * 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.tcm.membership; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.TypeSizes.sizeof; +import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt; + +public class NodeVersion implements Comparable +{ + public static final Serializer serializer = new Serializer(); + public static final Version CURRENT_METADATA_VERSION = Version.V2; + public static final NodeVersion CURRENT = new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), CURRENT_METADATA_VERSION); + private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_0; + + public final CassandraVersion cassandraVersion; + public final int serializationVersion; + + public NodeVersion(CassandraVersion cassandraVersion, Version serializationVersion) + { + assert serializationVersion != Version.UNKNOWN; + this.cassandraVersion = cassandraVersion; + this.serializationVersion = serializationVersion.asInt(); + } + + private NodeVersion(CassandraVersion cassandraVersion, int serializationVersion) + { + this.cassandraVersion = cassandraVersion; + this.serializationVersion = serializationVersion; + } + + public Version serializationVersion() + { + if (serializationVersion <= CURRENT.serializationVersion) + return Version.fromInt(serializationVersion); + + return Version.UNKNOWN; + } + + public boolean isUpgraded() + { + return serializationVersion >= Version.V0.asInt(); + } + + @Override + public String toString() + { + return "NodeVersion{" + + "cassandraVersion=" + cassandraVersion + + ", serializationVersion=" + serializationVersion + + '}'; + } + + @Override + public int compareTo(NodeVersion o) + { + // only comparing cassandraVersion here - if we bump serializationVersion we need to release a new cassandra version + return cassandraVersion.compareTo(o.cassandraVersion); + } + + public static NodeVersion fromCassandraVersion(CassandraVersion cv) + { + if (cv == null) + return CURRENT; + Version version = Version.OLD; + if (cv.compareTo(SINCE_VERSION, true) >= 0) + version = CURRENT_METADATA_VERSION; + return new NodeVersion(cv, version); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof NodeVersion)) return false; + NodeVersion that = (NodeVersion) o; + return Objects.equals(cassandraVersion, that.cassandraVersion) && serializationVersion == that.serializationVersion; + } + + @Override + public int hashCode() + { + return Objects.hash(cassandraVersion, serializationVersion); + } + + public static class Serializer implements MetadataSerializer + { + @Override + public void serialize(NodeVersion t, DataOutputPlus out, Version version) throws IOException + { + out.writeUTF(t.cassandraVersion.toString()); + if (t.serializationVersion == Version.UNKNOWN.asInt()) + throw new IllegalStateException("Should not serialize UNKNOWN version"); + out.writeUnsignedVInt32(t.serializationVersion); + } + + @Override + public NodeVersion deserialize(DataInputPlus in, Version version) throws IOException + { + CassandraVersion cassandraVersion = new CassandraVersion(in.readUTF()); + int serializationVersion = in.readUnsignedVInt32(); + return new NodeVersion(cassandraVersion, serializationVersion); + } + + @Override + public long serializedSize(NodeVersion t, Version version) + { + return sizeof(t.cassandraVersion.toString()) + + sizeofUnsignedVInt(t.serializationVersion); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/migration/ClusterMetadataHolder.java b/src/java/org/apache/cassandra/tcm/migration/ClusterMetadataHolder.java new file mode 100644 index 0000000000..83e94014c8 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/migration/ClusterMetadataHolder.java @@ -0,0 +1,95 @@ +/* + * 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.tcm.migration; + +import java.io.IOException; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.serialization.Version; + +public class ClusterMetadataHolder +{ + public static final ClusterMetadataHolder.Serializer defaultMessageSerializer = new ClusterMetadataHolder.Serializer(NodeVersion.CURRENT.serializationVersion()); + + private static volatile Serializer serializerCache; + public static IVersionedSerializer messageSerializer(Version version) + { + Serializer cached = serializerCache; + if (cached != null && cached.serializationVersion.equals(version)) + return cached; + cached = new Serializer(version); + serializerCache = cached; + return cached; + } + + public final Election.Initiator coordinator; + public final ClusterMetadata metadata; + + public ClusterMetadataHolder(Election.Initiator coordinator, ClusterMetadata metadata) + { + this.coordinator = coordinator; + this.metadata = metadata; + } + + @Override + public String toString() + { + return "ClusterMetadataHolder{" + + "coordinator=" + coordinator + + ", epoch=" + metadata.epoch + + '}'; + } + + private static class Serializer implements IVersionedSerializer + { + private final Version serializationVersion; + + public Serializer(Version serializationVersion) + { + this.serializationVersion = serializationVersion; + } + + @Override + public void serialize(ClusterMetadataHolder t, DataOutputPlus out, int version) throws IOException + { + Election.Initiator.serializer.serialize(t.coordinator, out, version); + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, t.metadata, out, serializationVersion); + } + + @Override + public ClusterMetadataHolder deserialize(DataInputPlus in, int version) throws IOException + { + Election.Initiator coordinator = Election.Initiator.serializer.deserialize(in, version); + ClusterMetadata metadata = VerboseMetadataSerializer.deserialize(ClusterMetadata.serializer, in); + return new ClusterMetadataHolder(coordinator, metadata); + } + + @Override + public long serializedSize(ClusterMetadataHolder t, int version) + { + return Election.Initiator.serializer.serializedSize(t.coordinator, version) + + VerboseMetadataSerializer.serializedSize(ClusterMetadata.serializer, t.metadata, serializationVersion); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/migration/Election.java b/src/java/org/apache/cassandra/tcm/migration/Election.java new file mode 100644 index 0000000000..8f45699c8d --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/migration/Election.java @@ -0,0 +1,264 @@ +/* + * 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.tcm.migration; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Startup; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.UUIDSerializer; + +/** + * Election process establishes initial CMS leader, from which you can further evolve cluster metadata. + */ +public class Election +{ + private static final Logger logger = LoggerFactory.getLogger(Election.class); + private static final Initiator MIGRATED = new Initiator(null, null); + + private final AtomicReference initiator = new AtomicReference<>(); + + public static Election instance = new Election(); + + public final PrepareHandler prepareHandler; + public final AbortHandler abortHandler; + + private final MessageDelivery messaging; + + private Election() + { + this(MessagingService.instance()); + } + + private Election(MessageDelivery messaging) + { + this.messaging = messaging; + this.prepareHandler = new PrepareHandler(); + this.abortHandler = new AbortHandler(); + } + + public void nominateSelf(Set candidates, Set ignoredEndpoints, Function isMatch, ClusterMetadata metadata) + { + Set sendTo = new HashSet<>(candidates); + sendTo.removeAll(ignoredEndpoints); + sendTo.remove(FBUtilities.getBroadcastAddressAndPort()); + + try + { + initiate(sendTo, isMatch, metadata); + finish(sendTo); + } + catch (Exception e) + { + abort(sendTo); + throw e; + } + } + + private void initiate(Set sendTo, Function isMatch, ClusterMetadata metadata) + { + if (!updateInitiator(null, new Initiator(FBUtilities.getBroadcastAddressAndPort(), UUID.randomUUID()))) + throw new IllegalStateException("Migration already initiated by " + initiator.get()); + + logger.info("No previous migration detected, initiating"); + Collection> metadatas = MessageDelivery.fanoutAndWait(messaging, sendTo, Verb.TCM_INIT_MIG_REQ, initiator.get()); + if (metadatas.size() != sendTo.size()) + { + Set responded = metadatas.stream().map(p -> p.left).collect(Collectors.toSet()); + String msg = String.format("Did not get response from %s - not continuing with migration. Ignore down hosts with --ignore ", Sets.difference(sendTo, responded)); + logger.warn(msg); + throw new IllegalStateException(msg); + } + + Set mismatching = metadatas.stream().filter(p -> !isMatch.apply(p.right.metadata)).map(p -> p.left).collect(Collectors.toSet()); + if (!mismatching.isEmpty()) + { + String msg = String.format("Got mismatching cluster metadatas from %s aborting migration", mismatching); + Map metadataMap = new HashMap<>(); + metadatas.forEach(pair -> metadataMap.put(pair.left, pair.right)); + if (metadata != null) + { + for (InetAddressAndPort e : mismatching) + { + logger.warn("Diff with {}", e); + metadata.dumpDiff(metadataMap.get(e).metadata); + } + } + throw new IllegalStateException(msg); + } + } + + private void finish(Set sendTo) + { + Initiator currentCoordinator = initiator.get(); + assert currentCoordinator.initiator.equals(FBUtilities.getBroadcastAddressAndPort()); + + Startup.initializeAsFirstCMSNode(); + Register.maybeRegister(); + + updateInitiator(currentCoordinator, MIGRATED); + MessageDelivery.fanoutAndWait(messaging, sendTo, Verb.TCM_NOTIFY_REQ, DistributedMetadataLogKeyspace.getLogState(Epoch.EMPTY, false)); + } + + private void abort(Set sendTo) + { + Initiator init = initiator.getAndSet(null); + for (InetAddressAndPort ep : sendTo) + messaging.send(Message.out(Verb.TCM_ABORT_MIG, init), ep); + } + + public Initiator initiator() + { + return initiator.get(); + } + + public void migrated() + { + initiator.set(MIGRATED); + } + + private boolean updateInitiator(Initiator expected, Initiator newCoordinator) + { + Initiator current = initiator.get(); + return Objects.equals(current, expected) && initiator.compareAndSet(current, newCoordinator); + } + + public boolean isMigrating() + { + Initiator coordinator = initiator(); + return coordinator != null && coordinator != MIGRATED; + } + + public class PrepareHandler implements IVerbHandler + { + @Override + public void doVerb(Message message) throws IOException + { + logger.info("Received election initiation message {} from {}", message.payload, message.from()); + if (!updateInitiator(null, message.payload)) + throw new IllegalStateException(String.format("Got duplicate initiate migration message from %s, migration is already started by %s", message.from(), initiator())); + + // todo; disallow ANY changes to state managed in ClusterMetadata + logger.info("Sending initiation response"); + messaging.send(message.responseWith(new ClusterMetadataHolder(message.payload, ClusterMetadata.current())), message.from()); + } + } + + public class AbortHandler implements IVerbHandler + { + @Override + public void doVerb(Message message) throws IOException + { + logger.info("Received election abort message {} from {}", message.payload, message.from()); + if (!message.from().equals(initiator().initiator) || !updateInitiator(message.payload, null)) + logger.error("Could not clear initiator - initiator is set to {}, abort message received from {}", initiator(), message.payload); + } + } + + public static class Initiator + { + public static final Serializer serializer = new Serializer(); + + public final InetAddressAndPort initiator; + public final UUID initToken; + + public Initiator(InetAddressAndPort initiator, UUID initToken) + { + this.initiator = initiator; + this.initToken = initToken; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Initiator)) return false; + Initiator other = (Initiator) o; + return Objects.equals(initiator, other.initiator) && Objects.equals(initToken, other.initToken); + } + + @Override + public int hashCode() + { + return Objects.hash(initiator, initToken); + } + + @Override + public String toString() + { + return "Initiator{" + + "initiator=" + initiator + + ", initToken=" + initToken + + '}'; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(Initiator t, DataOutputPlus out, int version) throws IOException + { + InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(t.initiator, out, version); + UUIDSerializer.serializer.serialize(t.initToken, out, version); + } + + @Override + public Initiator deserialize(DataInputPlus in, int version) throws IOException + { + return new Initiator(InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version), + UUIDSerializer.serializer.deserialize(in, version)); + } + + @Override + public long serializedSize(Initiator t, int version) + { + return InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(t.initiator, version) + + UUIDSerializer.serializer.serializedSize(t.initToken, version); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/migration/GossipCMSListener.java b/src/java/org/apache/cassandra/tcm/migration/GossipCMSListener.java new file mode 100644 index 0000000000..b33f4fbea1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/migration/GossipCMSListener.java @@ -0,0 +1,80 @@ +/* + * 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.tcm.migration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.IEndpointStateChangeSubscriber; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.utils.CassandraVersion; + +public class GossipCMSListener implements IEndpointStateChangeSubscriber +{ + private static final Logger logger = LoggerFactory.getLogger(GossipCMSListener.class); + @Override + public void onJoin(InetAddressAndPort endpoint, EndpointState epState) + { + ClusterMetadata metadata = ClusterMetadata.current(); + + if (!metadata.epoch.is(Epoch.UPGRADE_GOSSIP)) + return; + + NodeId nodeId = metadata.directory.peerId(endpoint); + if (nodeId == null) + { + logger.error("Unknown node {} started", endpoint); + return; + } + // only thing that can change is the release version + CassandraVersion gossipVersion = epState.getReleaseVersion(); + if (gossipVersion == null) + return; + while (true) + { + NodeVersion cmVersion = metadata.directory.versions.get(nodeId); + if (cmVersion.cassandraVersion.equals(gossipVersion)) + { + return; + } + else + { + NodeVersion newNodeVersion = NodeVersion.fromCassandraVersion(gossipVersion); + ClusterMetadata newCM = metadata.transformer() + .withVersion(nodeId, newNodeVersion) + .buildForGossipMode(); + if (ClusterMetadataService.instance().applyFromGossip(metadata, newCM)) + return; + metadata = ClusterMetadata.current(); + } + } + } + + @Override + public void onAlive(InetAddressAndPort endpoint, EndpointState state) + { + onJoin(endpoint, state); + } +} diff --git a/src/java/org/apache/cassandra/tcm/migration/GossipProcessor.java b/src/java/org/apache/cassandra/tcm/migration/GossipProcessor.java new file mode 100644 index 0000000000..beb71feb12 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/migration/GossipProcessor.java @@ -0,0 +1,42 @@ +/* + * 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.tcm.migration; + +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Processor; +import org.apache.cassandra.tcm.Retry; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ClusterMetadata; + +public class GossipProcessor implements Processor +{ + @Override + public Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy) + { + throw new IllegalStateException("Can't commit transformations when running in gossip mode. Enable the ClusterMetadataService with `nodetool addtocms`."); + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + return ClusterMetadata.current(); + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java b/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java new file mode 100644 index 0000000000..367945f058 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/DataPlacement.java @@ -0,0 +1,206 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class DataPlacement +{ + public static final Serializer serializer = new Serializer(); + + private static final DataPlacement EMPTY = new DataPlacement(PlacementForRange.EMPTY, PlacementForRange.EMPTY); + + // TODO make tree of just EndpointsForRange, navigable by EFR.range() + // TODO combine peers into a single entity with one vote in any quorum + // (e.g. old & new peer must both respond to count one replica) + public final PlacementForRange reads; + public final PlacementForRange writes; + + public DataPlacement(PlacementForRange reads, + PlacementForRange writes) + { + this.reads = reads; + this.writes = writes; + } + + /** + * A union of read and write endpoints for range, for watermark purposes + */ + public Set affectedReplicas(Range range) + { + Set endpoints = new HashSet<>(); + for (Replica r : reads.matchRange(range).get()) + endpoints.add(r.endpoint()); + for (Replica r : writes.matchRange(range).get()) + endpoints.add(r.endpoint()); + return endpoints; + } + + public DataPlacement combineReplicaGroups(DataPlacement other) + { + return new DataPlacement(PlacementForRange.builder() + .withReplicaGroups(reads.replicaGroups().values()) + .withReplicaGroups(other.reads.replicaGroups.values()) + .build(), + PlacementForRange.builder() + .withReplicaGroups(writes.replicaGroups().values()) + .withReplicaGroups(other.writes.replicaGroups.values()) + .build()); + } + + public PlacementDeltas.PlacementDelta difference(DataPlacement next) + { + return new PlacementDeltas.PlacementDelta(reads.difference(next.reads), + writes.difference(next.writes)); + } + + public DataPlacement splitRangesForPlacement(List tokens) + { + return new DataPlacement(PlacementForRange.splitRangesForPlacement(tokens, reads), + PlacementForRange.splitRangesForPlacement(tokens, writes)); + } + + public static DataPlacement empty() + { + return EMPTY; + } + + public static Builder builder() + { + return new Builder(PlacementForRange.builder(), + PlacementForRange.builder()); + } + + public Builder unbuild() + { + return new Builder(reads.unbuild(), writes.unbuild()); + } + public static class Builder + { + public final PlacementForRange.Builder reads; + public final PlacementForRange.Builder writes; + + public Builder(PlacementForRange.Builder reads, PlacementForRange.Builder writes) + { + this.reads = reads; + this.writes = writes; + } + + public Builder withWriteReplica(Epoch epoch, Replica replica) + { + this.writes.withReplica(epoch, replica); + return this; + } + + public Builder withoutWriteReplica(Epoch epoch, Replica replica) + { + this.writes.withoutReplica(epoch, replica); + return this; + } + + public Builder withReadReplica(Epoch epoch, Replica replica) + { + this.reads.withReplica(epoch, replica); + return this; + } + + public Builder withoutReadReplica(Epoch epoch, Replica replica) + { + this.reads.withoutReplica(epoch, replica); + return this; + } + + public DataPlacement build() + { + return new DataPlacement(reads.build(), writes.build()); + } + } + + @Override + public String toString() + { + return "DataPlacement{" + + "reads=" + toString(reads.replicaGroups) + + ", writes=" + toString(writes.replicaGroups) + + '}'; + } + + public static String toString(Map predicted) + { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : predicted.entrySet()) + { + sb.append(e.getKey()).append("=").append(e.getValue()).append(",\n"); + } + + return sb.toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof DataPlacement)) return false; + DataPlacement that = (DataPlacement) o; + return Objects.equals(reads, that.reads) && Objects.equals(writes, that.writes); + } + + @Override + public int hashCode() + { + return Objects.hash(reads, writes); + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(DataPlacement t, DataOutputPlus out, Version version) throws IOException + { + PlacementForRange.serializer.serialize(t.reads, out, version); + PlacementForRange.serializer.serialize(t.writes, out, version); + } + + public DataPlacement deserialize(DataInputPlus in, Version version) throws IOException + { + PlacementForRange reads = PlacementForRange.serializer.deserialize(in, version); + PlacementForRange writes = PlacementForRange.serializer.deserialize(in, version); + return new DataPlacement(reads, writes); + } + + public long serializedSize(DataPlacement t, Version version) + { + return PlacementForRange.serializer.serializedSize(t.reads, version) + + PlacementForRange.serializer.serializedSize(t.writes, version); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/DataPlacements.java b/src/java/org/apache/cassandra/tcm/ownership/DataPlacements.java new file mode 100644 index 0000000000..cd427dbb9e --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/DataPlacements.java @@ -0,0 +1,277 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.TypeSizes.sizeof; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +public class DataPlacements extends ReplicationMap implements MetadataValue +{ + public static Serializer serializer = new Serializer(); + + public static final DataPlacements EMPTY = DataPlacements.builder(1).build(); + private static DataPlacement LOCAL_PLACEMENT; + + private final Epoch lastModified; + + private DataPlacements(Epoch lastModified, Map map) + { + super(map); + this.lastModified = lastModified; + } + + public DataPlacements replaceParams(Epoch lastModified, ReplicationParams oldParams, ReplicationParams newParams) + { + Map newMap = Maps.newHashMapWithExpectedSize(map.size()); + assert map.containsKey(oldParams) : String.format("Can't replace key %s, since map doesn't contain it: %s", oldParams, map); + for (Map.Entry e : map.entrySet()) + { + if (e.getKey().equals(oldParams)) + newMap.put(newParams, e.getValue()); + else + newMap.put(e.getKey(), e.getValue()); + } + + return new DataPlacements(lastModified, newMap); + } + + protected DataPlacement defaultValue() + { + return DataPlacement.empty(); + } + + public void withDistributed(BiConsumer consumer) + { + forEach(e -> { + if (e.getKey().isLocal() || e.getKey().isMeta()) + return; + + consumer.accept(e.getKey(), e.getValue()); + }); + } + + protected DataPlacement localOnly() + { + // it's unlikely to happen, but perfectly safe to create multiple times, so no need to lock or statically init + if (null == LOCAL_PLACEMENT) + { + PlacementForRange placement = new PlacementForRange(Collections.singletonMap(entireRange, + VersionedEndpoints.forRange(Epoch.EMPTY, + EndpointsForRange.of(EntireRange.replica(FBUtilities.getBroadcastAddressAndPort()))))); + LOCAL_PLACEMENT = new DataPlacement(placement, placement); + } + return LOCAL_PLACEMENT; + } + + public DataPlacements combineReplicaGroups(DataPlacements end) + { + DataPlacements start = this; + if (start.isEmpty()) + return end; + Builder mapBuilder = DataPlacements.builder(start.size()); + start.asMap().forEach((params, placement) -> + mapBuilder.with(params, placement.combineReplicaGroups(end.get(params)))); + return mapBuilder.build(); + } + + @Override + public DataPlacements withLastModified(Epoch epoch) + { + return new DataPlacements(epoch, asMap()); + } + + @Override + public Epoch lastModified() + { + return lastModified; + } + + @Override + public String toString() + { + return "DataPlacements{" + + "lastModified=" + lastModified + + ", placementMap=" + asMap() + + '}'; + } + + public static DataPlacements sortReplicaGroups(DataPlacements placements, Comparator comparator) + { + Builder builder = DataPlacements.builder(placements.size()); + placements.forEach((params, placement) -> { + if (params.isMeta() || params.isLocal()) + builder.with(params, placement); + else + { + PlacementForRange.Builder reads = PlacementForRange.builder(placement.reads.replicaGroups().size()); + placement.reads.replicaGroups().forEach((range, endpoints) -> { + reads.withReplicaGroup(VersionedEndpoints.forRange(endpoints.lastModified(), + endpoints.get().sorted(comparator))); + }); + PlacementForRange.Builder writes = PlacementForRange.builder(placement.writes.replicaGroups().size()); + placement.writes.replicaGroups().forEach((range, endpoints) -> { + writes.withReplicaGroup(VersionedEndpoints.forRange(endpoints.lastModified(), + endpoints.get().sorted(comparator))); + }); + builder.with(params, new DataPlacement(reads.build(), writes.build())); + } + }); + return builder.build(); + } + + public DataPlacements applyDelta(Epoch epoch, PlacementDeltas deltas) + { + return deltas.apply(epoch, this); + } + + public static DataPlacements empty() + { + return EMPTY; + } + + public static Builder builder(int expectedSize) + { + return new Builder(new HashMap<>(expectedSize)); + } + + public static Builder builder(Map map) + { + return new Builder(map); + } + + public Builder unbuild() + { + return new Builder(new HashMap<>(this.asMap())); + } + + public static class Builder + { + private final Map map; + private Builder(Map map) + { + this.map = map; + } + + public Builder with(ReplicationParams params, DataPlacement placement) + { + map.put(params, placement); + return this; + } + + public DataPlacements build() + { + return new DataPlacements(Epoch.EMPTY, map); + } + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(DataPlacements t, DataOutputPlus out, Version version) throws IOException + { + Map map = t.asMap(); + out.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) + { + ReplicationParams.serializer.serialize(entry.getKey(), out, version); + DataPlacement.serializer.serialize(entry.getValue(), out, version); + } + Epoch.serializer.serialize(t.lastModified, out, version); + } + + public DataPlacements deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + Map map = Maps.newHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) + { + ReplicationParams params = ReplicationParams.serializer.deserialize(in, version); + DataPlacement dp = DataPlacement.serializer.deserialize(in, version); + map.put(params, dp); + } + Epoch lastModified = Epoch.serializer.deserialize(in, version); + return new DataPlacements(lastModified, map); + } + + public long serializedSize(DataPlacements t, Version version) + { + int size = sizeof(t.size()); + for (Map.Entry entry : t.asMap().entrySet()) + { + size += ReplicationParams.serializer.serializedSize(entry.getKey(), version); + size += DataPlacement.serializer.serializedSize(entry.getValue(), version); + } + size += Epoch.serializer.serializedSize(t.lastModified, version); + return size; + } + } + + public void dumpDiff(DataPlacements other) + { + if (!map.equals(other.map)) + { + logger.warn("Maps differ: {} != {}", map, other.map); + dumpDiff(logger,map, other.map); + } + } + + private static final Logger logger = LoggerFactory.getLogger(DataPlacements.class); + public static void dumpDiff(Logger logger, Map l, Map r) + { + for (ReplicationParams k : Sets.intersection(l.keySet(), r.keySet())) + { + DataPlacement lv = l.get(k); + DataPlacement rv = r.get(k); + if (!Objects.equals(lv, rv)) + { + logger.warn("Values for key {} differ: {} != {}", k, lv, rv); + logger.warn("Difference: {}", lv.difference(rv)); + } + } + for (ReplicationParams k : Sets.difference(l.keySet(), r.keySet())) + logger.warn("Value for key {} is only present in the left set: {}", k, l.get(k)); + for (ReplicationParams k : Sets.difference(r.keySet(), l.keySet())) + logger.warn("Value for key {} is only present in the right set: {}", k, r.get(k)); + + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/Delta.java b/src/java/org/apache/cassandra/tcm/ownership/Delta.java new file mode 100644 index 0000000000..9660bd255a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/Delta.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.ownership; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class Delta +{ + public static final Serializer serializer = new Serializer(); + + private static final Delta EMPTY = new Delta(RangesByEndpoint.EMPTY, RangesByEndpoint.EMPTY); + + public final RangesByEndpoint removals; + public final RangesByEndpoint additions; + + public Delta(RangesByEndpoint removals, RangesByEndpoint additions) + { + this.removals = removals; + this.additions = additions; + } + + public Delta onlyAdditions() + { + return new Delta(RangesByEndpoint.EMPTY, additions); + } + + public Delta onlyRemovals() + { + return new Delta(removals, RangesByEndpoint.EMPTY); + } + + /** + * Merges this delta with `other` + * + * Note that if opposite operations (add a range in this, remove it in other for example) exist in + * `this` and `other` the operations cancel eachother out and neither will be in the resulting delta. + * @param other + * @return + */ + public Delta merge(Delta other) + { + RangesByEndpoint.Builder removalsBuilder = new RangesByEndpoint.Builder(); + RangesByEndpoint.Builder additionsBuilder = new RangesByEndpoint.Builder(); + addChange(removals, other.additions, removalsBuilder); + addChange(other.removals, additions, removalsBuilder); + addChange(additions, other.removals, additionsBuilder); + addChange(other.additions, removals, additionsBuilder); + return new Delta(removalsBuilder.build(), + additionsBuilder.build()); + } + + private static void addChange(RangesByEndpoint change, RangesByEndpoint opposite, RangesByEndpoint.Builder builder) + { + change.asMap().forEach((ep, replicas) -> { + replicas.forEach(replica -> { + if (!opposite.get(ep).contains(replica) && !builder.get(ep).contains(replica)) + builder.put(ep, replica); + }); + }); + } + + public Delta invert() + { + return new Delta(additions, removals); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Delta delta = (Delta) o; + + return Objects.equals(removals, delta.removals) && Objects.equals(additions, delta.additions); + } + + public int hashCode() + { + return Objects.hash(removals, additions); + } + + @Override + public String toString() + { + return "Delta{" + + "removals=" + removals + + ", additions=" + additions + + '}'; + } + + public static Delta empty() + { + return EMPTY; + } + + public static final class Serializer implements MetadataSerializer + { + public void serialize(Delta t, DataOutputPlus out, Version version) throws IOException + { + RangesByEndpoint.serializer.serialize(t.removals, out, version); + RangesByEndpoint.serializer.serialize(t.additions, out, version); + } + + public Delta deserialize(DataInputPlus in, Version version) throws IOException + { + return new Delta(RangesByEndpoint.serializer.deserialize(in, version), + RangesByEndpoint.serializer.deserialize(in, version)); + } + + public long serializedSize(Delta t, Version version) + { + return RangesByEndpoint.serializer.serializedSize(t.removals, version) + + RangesByEndpoint.serializer.serializedSize(t.additions, version); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/EntireRange.java b/src/java/org/apache/cassandra/tcm/ownership/EntireRange.java new file mode 100644 index 0000000000..26bd3f0744 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/EntireRange.java @@ -0,0 +1,50 @@ +/* + * 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.tcm.ownership; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.utils.FBUtilities; + +public class EntireRange +{ + public static final Range entireRange = new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), + DatabaseDescriptor.getPartitioner().getMinimumToken()); + public static LockedRanges.AffectedRanges affectedRanges(ClusterMetadata metadata) + { + return LockedRanges.AffectedRanges.singleton(ReplicationParams.meta(metadata), entireRange); + } + + public static Replica replica(InetAddressAndPort addr) + { + return new Replica(addr, entireRange, true); + } + + public static final EndpointsForRange localReplicas = EndpointsForRange.of(replica(FBUtilities.getBroadcastAddressAndPort())); + public static final DataPlacement placement = new DataPlacement(PlacementForRange.builder().withReplicaGroup(VersionedEndpoints.forRange(Epoch.FIRST, localReplicas)).build(), + PlacementForRange.builder().withReplicaGroup(VersionedEndpoints.forRange(Epoch.FIRST, localReplicas)).build()); +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/ownership/MovementMap.java b/src/java/org/apache/cassandra/tcm/ownership/MovementMap.java new file mode 100644 index 0000000000..7337f83180 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/MovementMap.java @@ -0,0 +1,165 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.Maps; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; + +public class MovementMap extends ReplicationMap +{ + private static final MovementMap EMPTY = new MovementMap(Collections.emptyMap()); + public static final IVersionedSerializer serializer = new Serializer(); + + private MovementMap(Map map) + { + super(map); + } + + protected EndpointsByReplica defaultValue() + { + return new EndpointsByReplica.Builder().build(); + } + + protected EndpointsByReplica localOnly() + { + throw new IllegalStateException("Cannot move local ranges"); + } + + public String toString() + { + return "MovementMap{" + + "map=" + asMap() + + '}'; + } + + public static MovementMap empty() + { + return EMPTY; + } + + public Map byEndpoint() + { + Map> builder = new HashMap<>(); + map.forEach((params, endpointsByReplica) -> { + for (Replica dst : endpointsByReplica.keySet()) + { + Map cur = builder.computeIfAbsent(dst.endpoint(), k -> new HashMap<>()); + EndpointsByReplica.Builder curBuilder = cur.computeIfAbsent(params, k -> new EndpointsByReplica.Builder()); + for (Replica src : endpointsByReplica.get(dst)) + curBuilder.put(dst, src); + } + }); + // and .build the builders created above: + Map built = new HashMap<>(); + for (Map.Entry> entry : builder.entrySet()) + { + InetAddressAndPort endpoint = entry.getKey(); + Map builders = entry.getValue(); + + Map byParams = new HashMap<>(); + for (Map.Entry builderEntry : builders.entrySet()) + byParams.put(builderEntry.getKey(), builderEntry.getValue().build()); + built.put(endpoint, new MovementMap(byParams)); + } + return built; + } + + public static Builder builder() + { + return new Builder(new HashMap<>()); + } + + public static Builder builder(int expectedSize) + { + return new Builder(Maps.newHashMapWithExpectedSize(expectedSize)); + } + + public static class Builder + { + private final Map map; + private Builder(Map map) + { + this.map = map; + } + + public Builder put(ReplicationParams params, EndpointsByReplica placement) + { + map.put(params, placement); + return this; + } + + public MovementMap build() + { + return new MovementMap(map); + } + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(MovementMap t, DataOutputPlus out, int version) throws IOException + { + out.writeUnsignedVInt32(t.size()); + for (Map.Entry entry : t.asMap().entrySet()) + { + ReplicationParams.messageSerializer.serialize(entry.getKey(), out, version); + EndpointsByReplica.serializer.serialize(entry.getValue(), out, version); + } + } + + @Override + public MovementMap deserialize(DataInputPlus in, int version) throws IOException + { + int size = in.readUnsignedVInt32(); + MovementMap.Builder builder = MovementMap.builder(size); + for (int i = 0; i < size; i++) + { + ReplicationParams params = ReplicationParams.messageSerializer.deserialize(in, version); + EndpointsByReplica endpointsByReplica = EndpointsByReplica.serializer.deserialize(in, version); + builder.put(params, endpointsByReplica); + } + return builder.build(); + } + + @Override + public long serializedSize(MovementMap t, int version) + { + long size = TypeSizes.sizeofVInt(t.size()); + for (Map.Entry entry : t.asMap().entrySet()) + { + size += ReplicationParams.messageSerializer.serializedSize(entry.getKey(), version); + size += EndpointsByReplica.serializer.serializedSize(entry.getValue(), version); + } + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/PlacementDeltas.java b/src/java/org/apache/cassandra/tcm/ownership/PlacementDeltas.java new file mode 100644 index 0000000000..6b5b817984 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/PlacementDeltas.java @@ -0,0 +1,245 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class PlacementDeltas extends ReplicationMap +{ + public static final Serializer serializer = new Serializer(); + private static final PlacementDeltas EMPTY = new PlacementDeltas(Collections.emptyMap()); + + private PlacementDeltas(Map map) + { + super(map); + } + + protected PlacementDelta defaultValue() + { + return PlacementDelta.EMPTY; + } + + protected PlacementDelta localOnly() + { + throw new IllegalStateException("Cannot apply diff to local placements."); + } + + public PlacementDeltas invert() + { + Builder inverse = builder(size()); + asMap().forEach((params, delta) -> inverse.put(params, delta.invert())); + return inverse.build(); + } + + @Override + public String toString() + { + return "DeltaMap{" + + "map=" + asMap() + + '}'; + } + + public DataPlacements apply(Epoch epoch, DataPlacements placements) + { + DataPlacements.Builder builder = placements.unbuild(); + asMap().forEach((params, delta) -> { + DataPlacement previous = placements.get(params); + builder.with(params, delta.apply(epoch, previous)); + }); + return builder.build(); + } + + public static PlacementDeltas empty() + { + return EMPTY; + } + + public static Builder builder() + { + return new Builder(new HashMap<>()); + } + + public static Builder builder(int expectedSize) + { + return new Builder(new HashMap<>(expectedSize)); + } + + public static Builder builder(Map map) + { + return new Builder(map); + } + + public static class PlacementDelta + { + public static PlacementDelta EMPTY = new PlacementDelta(Delta.empty(), Delta.empty()); + + public final Delta reads; + public final Delta writes; + + public PlacementDelta(Delta reads, Delta writes) + { + this.reads = reads; + this.writes = writes; + } + + public PlacementDelta onlyReads() + { + return new PlacementDelta(reads, Delta.empty()); + } + + public PlacementDelta onlyWrites() + { + return new PlacementDelta(Delta.empty(), writes); + } + + public PlacementDelta onlyAdditions() + { + return new PlacementDelta(reads.onlyAdditions(), writes.onlyAdditions()); + } + + public PlacementDelta onlyRemovals() + { + return new PlacementDelta(reads.onlyRemovals(), writes.onlyRemovals()); + } + + public DataPlacement apply(Epoch epoch, DataPlacement placement) + { + DataPlacement.Builder builder = placement.unbuild(); + reads.removals.flattenValues().forEach(r -> builder.reads.withoutReplica(epoch, r)); + writes.removals.flattenValues().forEach(r -> builder.writes.withoutReplica(epoch, r)); + + reads.additions.flattenValues().forEach(r -> builder.reads.withReplica(epoch, r)); + writes.additions.flattenValues().forEach(r -> builder.writes.withReplica(epoch, r)); + return builder.build(); + } + + public PlacementDelta merge(PlacementDelta other) + { + return new PlacementDelta(reads.merge(other.reads), + writes.merge(other.writes)); + } + + public PlacementDelta invert() + { + return new PlacementDelta(reads.invert(), writes.invert()); + } + + public String toString() + { + return "PlacementDelta{" + + "reads=" + reads + + ", writes=" + writes + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PlacementDelta other = (PlacementDelta) o; + + return Objects.equals(reads, other.reads) && Objects.equals(writes, other.writes); + } + + @Override + public int hashCode() + { + return Objects.hash(reads, writes); + } + } + + public static class Builder + { + private final Map map; + private Builder(Map map) + { + this.map = map; + } + + public Builder put(ReplicationParams params, PlacementDelta placement) + { + PlacementDelta delta = map.get(params); + if (delta == null) + map.put(params, placement); + else + map.put(params, delta.merge(placement)); + return this; + } + + public PlacementDeltas build() + { + return new PlacementDeltas(map); + } + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(PlacementDeltas t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.size()); + for (Map.Entry e : t.asMap().entrySet()) + { + ReplicationParams.serializer.serialize(e.getKey(), out, version); + Delta.serializer.serialize(e.getValue().reads, out, version); + Delta.serializer.serialize(e.getValue().writes, out, version); + } + + } + + public PlacementDeltas deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + Builder builder = PlacementDeltas.builder(size); + for (int i = 0; i < size; i++) + { + ReplicationParams replicationParams = ReplicationParams.serializer.deserialize(in, version); + Delta reads = Delta.serializer.deserialize(in, version); + Delta writes = Delta.serializer.deserialize(in, version); + builder.put(replicationParams, new PlacementDelta(reads, writes)); + } + return builder.build(); + } + + public long serializedSize(PlacementDeltas t, Version version) + { + long size = TypeSizes.INT_SIZE; + for (Map.Entry e : t.asMap().entrySet()) + { + size += ReplicationParams.serializer.serializedSize(e.getKey(), version); + size += Delta.serializer.serializedSize(e.getValue().reads, version); + size += Delta.serializer.serializedSize(e.getValue().writes, version); + } + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/PlacementForRange.java b/src/java/org/apache/cassandra/tcm/ownership/PlacementForRange.java new file mode 100644 index 0000000000..4aaa1089f5 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/PlacementForRange.java @@ -0,0 +1,437 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.AbstractReplicaCollection; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + +public class PlacementForRange +{ + public static final Serializer serializer = new Serializer(); + + public static final PlacementForRange EMPTY = PlacementForRange.builder().build(); + + final SortedMap, VersionedEndpoints.ForRange> replicaGroups; + + public PlacementForRange(Map, VersionedEndpoints.ForRange> replicaGroups) + { + this.replicaGroups = new TreeMap<>(replicaGroups); + } + + @VisibleForTesting + public Map, VersionedEndpoints.ForRange> replicaGroups() + { + return Collections.unmodifiableMap(replicaGroups); + } + + @VisibleForTesting + public List> ranges() + { + List> ranges = new ArrayList<>(replicaGroups.keySet()); + ranges.sort(Range::compareTo); + return ranges; + } + + @VisibleForTesting + public VersionedEndpoints.ForRange forRange(Range range) + { + // can't use range.isWrapAround() since range.unwrap() returns a wrapping range (right token is min value) + assert range.right.compareTo(range.left) > 0 || range.right.equals(range.right.minValue()); + return replicaGroups.get(range); + } + + /** + * This method is intended to be used on read/write path, not forRange. + */ + public VersionedEndpoints.ForRange matchRange(Range range) + { + EndpointsForRange.Builder builder = new EndpointsForRange.Builder(range); + Epoch lastModified = Epoch.EMPTY; + + for (Map.Entry, VersionedEndpoints.ForRange> entry : replicaGroups.entrySet()) + { + if (entry.getKey().contains(range)) + { + lastModified = Epoch.max(lastModified, entry.getValue().lastModified()); + builder.addAll(entry.getValue().get(), ReplicaCollection.Builder.Conflict.ALL); + } + } + + return VersionedEndpoints.forRange(lastModified, builder.build()); + } + + public VersionedEndpoints.ForRange forRange(Token token) + { + for (Map.Entry, VersionedEndpoints.ForRange> entry : replicaGroups.entrySet()) + { + if (entry.getKey().contains(token)) + return entry.getValue(); + } + throw new IllegalStateException("Could not find range for token " + token + " in PlacementForRange: " + replicaGroups); + } + + public VersionedEndpoints.ForToken forToken(Token token) + { + for (Map.Entry, VersionedEndpoints.ForRange> entry : replicaGroups.entrySet()) + { + if (entry.getKey().contains(token)) + return entry.getValue().forToken(token); + } + throw new IllegalStateException("Could not find range for token " + token + " in PlacementForRange: " + replicaGroups); + } + + public Delta difference(PlacementForRange next) + { + RangesByEndpoint oldMap = this.byEndpoint(); + RangesByEndpoint newMap = next.byEndpoint(); + return new Delta(diff(oldMap, newMap), diff(newMap, oldMap)); + } + + @VisibleForTesting + public RangesByEndpoint byEndpoint() + { + RangesByEndpoint.Builder builder = new RangesByEndpoint.Builder(); + for (Map.Entry, VersionedEndpoints.ForRange> oldPlacement : this.replicaGroups.entrySet()) + oldPlacement.getValue().byEndpoint().forEach(builder::put); + return builder.build(); + } + + private RangesByEndpoint diff(RangesByEndpoint left, RangesByEndpoint right) + { + RangesByEndpoint.Builder builder = new RangesByEndpoint.Builder(); + for (Map.Entry endPointRanges : left.entrySet()) + { + InetAddressAndPort endpoint = endPointRanges.getKey(); + RangesAtEndpoint leftRanges = endPointRanges.getValue(); + RangesAtEndpoint rightRanges = right.get(endpoint); + for (Replica leftReplica : leftRanges) + { + if (!rightRanges.contains(leftReplica)) + builder.put(endpoint, leftReplica); + } + } + return builder.build(); + } + + @Override + public String toString() + { + return replicaGroups.toString(); + } + + @VisibleForTesting + public Map> toStringByEndpoint() + { + Map> mappings = new HashMap<>(); + for (Map.Entry entry : byEndpoint().entrySet()) + mappings.put(entry.getKey().toString(), entry.getValue().asList(r -> r.range().toString())); + return mappings; + } + + @VisibleForTesting + public List toReplicaStringList() + { + return replicaGroups.values() + .stream() + .map(VersionedEndpoints.ForRange::get) + .flatMap(AbstractReplicaCollection::stream) + .map(Replica::toString) + .collect(Collectors.toList()); + } + + public Builder unbuild() + { + return new Builder(new HashMap<>(replicaGroups)); + } + + public static Builder builder() + { + return new Builder(); + } + + public static Builder builder(int expectedSize) + { + return new Builder(expectedSize); + } + + @VisibleForTesting + public static PlacementForRange splitRangesForPlacement(List tokens, PlacementForRange placement) + { + if (placement.replicaGroups.isEmpty()) + return placement; + + Builder newPlacement = PlacementForRange.builder(); + List eprs = new ArrayList<>(placement.replicaGroups.values()); + eprs.sort(Comparator.comparing(a -> a.range().left)); + Token min = eprs.get(0).range().left; + Token max = eprs.get(eprs.size() - 1).range().right; + + // if any token is < the start or > the end of the ranges covered, error + if (tokens.get(0).compareTo(min) < 0 || (!max.equals(min) && tokens.get(tokens.size()-1).compareTo(max) > 0)) + throw new IllegalArgumentException("New tokens exceed total bounds of current placement ranges " + tokens + " " + eprs); + Iterator iter = eprs.iterator(); + VersionedEndpoints.ForRange current = iter.next(); + for (Token token : tokens) + { + // handle special case where one of the tokens is the min value + if (token.equals(min)) + continue; + + assert current != null : tokens + " " + eprs; + Range r = current.get().range(); + int cmp = token.compareTo(r.right); + if (cmp == 0) + { + newPlacement.withReplicaGroup(current); + if (iter.hasNext()) + current = iter.next(); + else + current = null; + } + else if (cmp < 0 || r.right.isMinimum()) + { + Range left = new Range<>(r.left, token); + Range right = new Range<>(token, r.right); + newPlacement.withReplicaGroup(VersionedEndpoints.forRange(current.lastModified(), + EndpointsForRange.builder(left) + .addAll(current.get().asList(rep->rep.decorateSubrange(left))) + .build())); + current = VersionedEndpoints.forRange(current.lastModified(), + EndpointsForRange.builder(right) + .addAll(current.get().asList(rep->rep.decorateSubrange(right))) + .build()); + } + } + + if (current != null) + newPlacement.withReplicaGroup(current); + + return newPlacement.build(); + } + + public static class Builder + { + private final Map, VersionedEndpoints.ForRange> replicaGroups; + + private Builder() + { + this(new HashMap<>()); + } + + private Builder(int expectedSize) + { + this(new HashMap<>(expectedSize)); + } + + private Builder(Map, VersionedEndpoints.ForRange> replicaGroups) + { + this.replicaGroups = replicaGroups; + } + + public Builder withReplica(Epoch epoch, Replica replica) + { + VersionedEndpoints.ForRange group = + replicaGroups.computeIfPresent(replica.range(), + (t, v) -> { + EndpointsForRange old = v.get(); + return VersionedEndpoints.forRange(epoch, + old.newBuilder(old.size() + 1) + .addAll(old) + .add(replica, ReplicaCollection.Builder.Conflict.NONE) + .build()); + }); +; if (group == null) + replicaGroups.put(replica.range(), VersionedEndpoints.forRange(epoch, EndpointsForRange.of(replica))); + return this; + + } + + public Builder withoutReplica(Epoch epoch, Replica replica) + { + Range range = replica.range(); + VersionedEndpoints.ForRange group = replicaGroups.get(range); + if (group == null) + throw new IllegalArgumentException(String.format("No group found for range of supplied replica %s (%s)", + replica, range)); + EndpointsForRange without = group.get().without(Collections.singleton(replica.endpoint())); + if (without.isEmpty()) + replicaGroups.remove(range); + else + replicaGroups.put(range, VersionedEndpoints.forRange(epoch, without)); + return this; + } + + public Builder withReplicaGroup(VersionedEndpoints.ForRange replicas) + { + VersionedEndpoints.ForRange group = + replicaGroups.computeIfPresent(replicas.range(), + (t, v) -> { + EndpointsForRange old = v.get(); + return VersionedEndpoints.forRange(Epoch.max(v.lastModified(), replicas.lastModified()), + replicas.get() + .newBuilder(replicas.size() + old.size()) + .addAll(old) + .addAll(replicas.get(), ReplicaCollection.Builder.Conflict.ALL) + .build()); + }); + if (group == null) + replicaGroups.put(replicas.range(), replicas); + return this; + } + + public Builder withReplicaGroups(Iterable replicas) + { + replicas.forEach(this::withReplicaGroup); + return this; + } + + public PlacementForRange build() + { + return new PlacementForRange(this.replicaGroups); + } + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(PlacementForRange t, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(t.replicaGroups.size()); + + for (Map.Entry, VersionedEndpoints.ForRange> entry : t.replicaGroups.entrySet()) + { + Range range = entry.getKey(); + VersionedEndpoints.ForRange efr = entry.getValue(); + if (version.isAtLeast(Version.V2)) + Epoch.serializer.serialize(efr.lastModified(), out, version); + Token.metadataSerializer.serialize(range.left, out, version); + Token.metadataSerializer.serialize(range.right, out, version); + out.writeInt(efr.size()); + for (int i = 0; i < efr.size(); i++) + { + Replica r = efr.get().get(i); + Token.metadataSerializer.serialize(r.range().left, out, version); + Token.metadataSerializer.serialize(r.range().right, out, version); + InetAddressAndPort.MetadataSerializer.serializer.serialize(r.endpoint(), out, version); + out.writeBoolean(r.isFull()); + } + } + } + + public PlacementForRange deserialize(DataInputPlus in, Version version) throws IOException + { + int groupCount = in.readInt(); + Map, VersionedEndpoints.ForRange> result = Maps.newHashMapWithExpectedSize(groupCount); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < groupCount; i++) + { + Epoch lastModified; + if (version.isAtLeast(Version.V2)) + lastModified = Epoch.serializer.deserialize(in, version); + else + { + // During upgrade to V2, when reading from snapshot, we should start from the current version, rather than EMPTY + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata != null) + lastModified = metadata.epoch; + else + lastModified = Epoch.EMPTY; + } + Range range = new Range<>(Token.metadataSerializer.deserialize(in, partitioner, version), + Token.metadataSerializer.deserialize(in, partitioner, version)); + int replicaCount = in.readInt(); + List replicas = new ArrayList<>(replicaCount); + for (int x = 0; x < replicaCount; x++) + { + Range replicaRange = new Range<>(Token.metadataSerializer.deserialize(in, partitioner, version), + Token.metadataSerializer.deserialize(in, partitioner, version)); + InetAddressAndPort replicaAddress = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + boolean isFull = in.readBoolean(); + replicas.add(new Replica(replicaAddress, replicaRange, isFull)); + + } + EndpointsForRange efr = EndpointsForRange.copyOf(replicas); + result.put(range, VersionedEndpoints.forRange(lastModified, efr)); + } + return new PlacementForRange(result); + } + + public long serializedSize(PlacementForRange t, Version version) + { + int size = sizeof(t.replicaGroups.size()); + for (Map.Entry, VersionedEndpoints.ForRange> entry : t.replicaGroups.entrySet()) + { + Range range = entry.getKey(); + VersionedEndpoints.ForRange efr = entry.getValue(); + + if (version.isAtLeast(Version.V2)) + size += Epoch.serializer.serializedSize(efr.lastModified(), version); + size += Token.metadataSerializer.serializedSize(range.left, version); + size += Token.metadataSerializer.serializedSize(range.right, version); + size += sizeof(efr.size()); + for (int i = 0; i < efr.size(); i++) + { + Replica r = efr.get().get(i); + size += Token.metadataSerializer.serializedSize(r.range().left, version); + size += Token.metadataSerializer.serializedSize(r.range().right, version); + size += InetAddressAndPort.MetadataSerializer.serializer.serializedSize(r.endpoint(), version); + size += sizeof(r.isFull()); + } + } + return size; + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof PlacementForRange)) return false; + PlacementForRange that = (PlacementForRange) o; + return Objects.equals(replicaGroups, that.replicaGroups); + } + + @Override + public int hashCode() + { + return Objects.hash(replicaGroups); + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/PlacementProvider.java b/src/java/org/apache/cassandra/tcm/ownership/PlacementProvider.java new file mode 100644 index 0000000000..908bcf816e --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/PlacementProvider.java @@ -0,0 +1,54 @@ +/* + * 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.tcm.ownership; + +import java.util.List; +import java.util.Set; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeId; + +public interface PlacementProvider +{ + DataPlacements calculatePlacements(Epoch epoch, List> ranges, ClusterMetadata metadata, Keyspaces keyspaces); + // TODO naming + PlacementTransitionPlan planForJoin(ClusterMetadata metadata, + NodeId joining, + Set tokens, + Keyspaces keyspaces); + + PlacementTransitionPlan planForMove(ClusterMetadata metadata, + NodeId nodeId, + Set tokens, + Keyspaces keyspaces); + + // TODO: maybe leave, for consistency? + PlacementTransitionPlan planForDecommission(ClusterMetadata metadata, + NodeId nodeId, + Keyspaces keyspaces); + + PlacementTransitionPlan planForReplacement(ClusterMetadata metadata, + NodeId replaced, + NodeId replacement, + Keyspaces keyspaces); +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/PlacementTransitionPlan.java b/src/java/org/apache/cassandra/tcm/ownership/PlacementTransitionPlan.java new file mode 100644 index 0000000000..edb22919dc --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/PlacementTransitionPlan.java @@ -0,0 +1,128 @@ +/* + * 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.tcm.ownership; + +import org.apache.cassandra.tcm.sequences.LockedRanges; + +/** + * A transition plan contains four elements: + * - deltas to split original ranges, if necessary + * - deltas to move each placement from start to maximal (i.e. overreplicated) state + * - deltas to move each placement from maximal to final state + * - deltas to merge final ranges, if necessary + * These deltas describe the abstract changes necessary to transition between two one cluster states. + * When the plan is compiled, these deltas are deconstructed and recomposed into the steps necessary for + * safe execution of the given operation. + */ +public class PlacementTransitionPlan +{ + public final PlacementDeltas toSplit; + public final PlacementDeltas toMaximal; + public final PlacementDeltas toFinal; + public final PlacementDeltas toMerged; + + private PlacementDeltas addToWrites; + private PlacementDeltas moveReads; + private PlacementDeltas removeFromWrites; + private LockedRanges.AffectedRanges affectedRanges; + + public PlacementTransitionPlan(PlacementDeltas toSplit, + PlacementDeltas toMaximal, + PlacementDeltas toFinal, + PlacementDeltas toMerged) + { + this.toSplit = toSplit; + this.toMaximal = toMaximal; + this.toFinal = toFinal; + this.toMerged = toMerged; + } + + public PlacementDeltas addToWrites() + { + if (addToWrites == null) + compile(); + return addToWrites; + } + public PlacementDeltas moveReads() + { + if (moveReads == null) + compile(); + return moveReads; + } + + public PlacementDeltas removeFromWrites() + { + if (removeFromWrites == null) + compile(); + return removeFromWrites; + } + + public LockedRanges.AffectedRanges affectedRanges() + { + if (affectedRanges == null) + compile(); + return affectedRanges; + } + + private void compile() + { + PlacementDeltas.Builder addToWrites = PlacementDeltas.builder(); + PlacementDeltas.Builder moveReads = PlacementDeltas.builder(); + PlacementDeltas.Builder removeFromWrites = PlacementDeltas.builder(); + LockedRanges.AffectedRangesBuilder affectedRanges = LockedRanges.AffectedRanges.builder(); + + toSplit.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + }); + + toMaximal.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + addToWrites.put(replication, delta.onlyWrites()); + moveReads.put(replication, delta.onlyReads()); + }); + + toFinal.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + moveReads.put(replication, delta.onlyReads()); + removeFromWrites.put(replication, delta.onlyWrites()); + }); + + toMerged.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + removeFromWrites.put(replication, delta); + }); + this.addToWrites = addToWrites.build(); + this.moveReads = moveReads.build(); + this.removeFromWrites = removeFromWrites.build(); + this.affectedRanges = affectedRanges.build(); + + } + + @Override + public String toString() + { + return "PlacementTransitionPlan{" + + "toSplit=" + toSplit + + ", toMaximal=" + toMaximal + + ", toFinal=" + toFinal + + ", toMerged=" + toMerged + + ", compiled=" + (addToWrites == null) + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/PrimaryRangeComparator.java b/src/java/org/apache/cassandra/tcm/ownership/PrimaryRangeComparator.java new file mode 100644 index 0000000000..412c09bb59 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/PrimaryRangeComparator.java @@ -0,0 +1,51 @@ +/* + * 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.tcm.ownership; + +import java.util.Comparator; + +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; + +public class PrimaryRangeComparator implements Comparator +{ + private final TokenMap tokens; + private final Directory directory; + + public PrimaryRangeComparator(TokenMap tokens, Directory directory) + { + this.tokens = tokens; + this.directory = directory; + } + + @Override + public int compare(Replica o1, Replica o2) + { + assert o1.range().equals(o2.range()); + Token target = o1.range().right.equals(tokens.partitioner().getMinimumToken()) + ? tokens.tokens().get(0) + : o1.range().right; + NodeId owner = tokens.owner(target); + return directory.peerId(o1.endpoint()).equals(owner) + ? -1 + : directory.peerId(o2.endpoint()).equals(owner) ? 1 : 0; + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/ReplicationMap.java b/src/java/org/apache/cassandra/tcm/ownership/ReplicationMap.java new file mode 100644 index 0000000000..a94b4a2acf --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/ReplicationMap.java @@ -0,0 +1,106 @@ +/* + * 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.tcm.ownership; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.google.common.collect.ImmutableMap; + +import org.apache.cassandra.schema.ReplicationParams; + +public abstract class ReplicationMap implements Iterable> +{ + protected final Map map; + + protected ReplicationMap() + { + this(Collections.emptyMap()); + } + + protected ReplicationMap(Map map) + { + this.map = map; + } + + protected abstract T defaultValue(); + protected abstract T localOnly(); + + public T get(ReplicationParams params) + { + if (params.isLocal()) + return localOnly(); + return map.getOrDefault(params, defaultValue()); + } + + public int size() + { + return map.size(); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public void forEach(BiConsumer consumer) + { + for (Map.Entry entry : this) + consumer.accept(entry.getKey(), entry.getValue()); + } + + public ImmutableMap asMap() + { + return ImmutableMap.copyOf(map); + } + + public Set keys() + { + return map.keySet(); + } + + public Iterator> iterator() + { + return map.entrySet().iterator(); + } + + public Stream> stream() + { + return StreamSupport.stream(spliterator(), false); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReplicationMap that = (ReplicationMap) o; + return map.equals(that.map); + } + + public int hashCode() + { + return Objects.hash(map); + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/TokenMap.java b/src/java/org/apache/cassandra/tcm/ownership/TokenMap.java new file mode 100644 index 0000000000..d8dca5ff52 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/TokenMap.java @@ -0,0 +1,290 @@ +/* + * 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.tcm.ownership; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.BiMultiValMap; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.SortedBiMultiValMap; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + +public class TokenMap implements MetadataValue +{ + public static final Serializer serializer = new Serializer(); + + private static final Logger logger = LoggerFactory.getLogger(TokenMap.class); + + private final SortedBiMultiValMap map; + private final List tokens; + private final List> ranges; + // TODO: move partitioner to the users (SimpleStrategy and Uniform Range Placement?) + private final IPartitioner partitioner; + private final Epoch lastModified; + + public TokenMap(IPartitioner partitioner) + { + this(Epoch.EMPTY, partitioner, SortedBiMultiValMap.create()); + } + + private TokenMap(Epoch lastModified, IPartitioner partitioner, SortedBiMultiValMap map) + { + this.lastModified = lastModified; + this.partitioner = partitioner; + this.map = map; + this.tokens = tokens(); + this.ranges = toRanges(tokens, partitioner); + } + + @Override + public TokenMap withLastModified(Epoch epoch) + { + return new TokenMap(epoch, partitioner, map); + } + + @Override + public Epoch lastModified() + { + return lastModified; + } + + public TokenMap assignTokens(NodeId id, Collection tokens) + { + SortedBiMultiValMap finalisedCopy = SortedBiMultiValMap.create(map); + tokens.forEach(t -> finalisedCopy.putIfAbsent(t, id)); + return new TokenMap(lastModified, partitioner, finalisedCopy); + } + + public TokenMap unassignTokens(NodeId id) + { + SortedBiMultiValMap finalisedCopy = SortedBiMultiValMap.create(map); + finalisedCopy.removeValue(id); + return new TokenMap(lastModified, partitioner, finalisedCopy); + } + + public TokenMap unassignTokens(NodeId id, Collection tokens) + { + SortedBiMultiValMap finalisedCopy = SortedBiMultiValMap.create(map); + for (Token token : tokens) + { + NodeId nodeId = finalisedCopy.remove(token); + assert nodeId.equals(id); + } + + return new TokenMap(lastModified, partitioner, finalisedCopy); + } + + public BiMultiValMap asMap() + { + return SortedBiMultiValMap.create(map); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public IPartitioner partitioner() + { + return partitioner; + } + + public ImmutableList tokens() + { + return ImmutableList.copyOf(map.keySet()); + } + + public ImmutableList tokens(NodeId nodeId) + { + Collection tokens = map.inverse().get(nodeId); + if (tokens == null) + return null; + return ImmutableList.copyOf(tokens); + } + + public List> toRanges() + { + return ranges; + } + + public static List> toRanges(List tokens, IPartitioner partitioner) + { + if (tokens.isEmpty()) + return Collections.emptyList(); + + List> ranges = new ArrayList<>(tokens.size() + 1); + maybeAdd(ranges, new Range<>(partitioner.getMinimumToken(), tokens.get(0))); + for (int i = 1; i < tokens.size(); i++) + maybeAdd(ranges, new Range<>(tokens.get(i - 1), tokens.get(i))); + maybeAdd(ranges, new Range<>(tokens.get(tokens.size() - 1), partitioner.getMinimumToken())); + if (ranges.isEmpty()) + ranges.add(new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken())); + return ranges; + } + + private static void maybeAdd(List> ranges, Range r) + { + if (r.left.compareTo(r.right) != 0) + ranges.add(r); + } + + public Token nextToken(List tokens, Token token) + { + return tokens.get(nextTokenIndex(tokens, token)); + } + + //Duplicated from TokenMetadata::firstTokenIndex + public static int nextTokenIndex(final List ring, Token start) + { + assert ring.size() > 0; + int i = Collections.binarySearch(ring, start); + if (i < 0) + { + i = (i + 1) * (-1); + if (i >= ring.size()) + i = 0; + } + return i; + } + + public NodeId owner(Token token) + { + return map.get(token); + } + + public String toString() + { + return "TokenMap{" + + toDebugString() + + '}'; + } + + public void logDebugString() + { + logger.info(toDebugString()); + } + + public String toDebugString() + { + StringBuilder b = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) + b.append('[').append(entry.getKey()).append("] => ").append(entry.getValue()).append(";\n"); + return b.toString(); + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(TokenMap t, DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(t.lastModified, out, version); + out.writeUTF(t.partitioner.getClass().getCanonicalName()); + out.writeInt(t.map.size()); + for (Map.Entry entry : t.map.entrySet()) + { + Token.metadataSerializer.serialize(entry.getKey(), out, version); + NodeId.serializer.serialize(entry.getValue(), out, version); + } + } + + public TokenMap deserialize(DataInputPlus in, Version version) throws IOException + { + Epoch lastModified = Epoch.serializer.deserialize(in, version); + IPartitioner partitioner = FBUtilities.newPartitioner(in.readUTF()); + int size = in.readInt(); + SortedBiMultiValMap tokens = SortedBiMultiValMap.create(); + for (int i = 0; i < size; i++) + tokens.put(Token.metadataSerializer.deserialize(in, partitioner, version), + NodeId.serializer.deserialize(in, version)); + return new TokenMap(lastModified, partitioner, tokens); + } + + public long serializedSize(TokenMap t, Version version) + { + long size = Epoch.serializer.serializedSize(t.lastModified, version); + size += sizeof(t.partitioner.getClass().getCanonicalName()); + size += sizeof(t.map.size()); + for (Map.Entry entry : t.map.entrySet()) + { + size += Token.metadataSerializer.serializedSize(entry.getKey(), version); + size += NodeId.serializer.serializedSize(entry.getValue(), version); + } + return size; + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof TokenMap)) return false; + TokenMap tokenMap = (TokenMap) o; + return Objects.equals(lastModified, tokenMap.lastModified) && + isEquivalent(tokenMap); + } + + @Override + public int hashCode() + { + return Objects.hash(lastModified, map, partitioner); + } + + /** + * returns true if this token map is functionally equivalent to the given one + * + * does not check equality of lastModified + */ + public boolean isEquivalent(TokenMap tokenMap) + { + return Objects.equals(map, tokenMap.map) && + Objects.equals(partitioner, tokenMap.partitioner); + } + + public void dumpDiff(TokenMap other) + { + if (!Objects.equals(map, other.map)) + { + logger.warn("Maps differ: {} != {}", map, other.map); + Directory.dumpDiff(logger, map, other.map); + } + if (!Objects.equals(partitioner, other.partitioner)) + logger.warn("Partitioners differ: {} != {}", partitioner, other.partitioner); + } +} diff --git a/src/java/org/apache/cassandra/tcm/ownership/UniformRangePlacement.java b/src/java/org/apache/cassandra/tcm/ownership/UniformRangePlacement.java new file mode 100644 index 0000000000..3d4f22ea2f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/UniformRangePlacement.java @@ -0,0 +1,349 @@ +/* + * 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.tcm.ownership; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeId; + +/** + * The defining feature of this placement stategy is that all layouts (i.e. replication params) use the same + * set of ranges. So when splitting the current ranges, we only need to calculate the splits once and apply to + * all existing placements. + * + * Also, when using this strategy, the read and write placements should (eventually) be identical. While range + * movements/bootstraps/decommissions are in-flight, this will not be the case as the read and write replica + * sets will diverge while nodes are acquiring/relinquishing ranges. Although there may always be such operations + * ongoing, this is technically a temporary state. + * + * Because of this, when calculating the steps to transition between the current state and a proposed new state, + * we work from the associated TokenMaps, the assumption being that eventually both the read and write placements + * will converge and will, at that point, reflect those TokenMaps. + * This means that the starting point of each transition is the intended end state of the preceding transitions. + * + * To do this calculation, we create a canonical DataPlacement from the current TokenMap and split it according + * to the proposed tokens. As we iterate through and split the existing ranges, we construct a new DataPlacement for + * each currently defined. There is no movement of data between the initial and new placements, only splitting of + * existing ranges, so we can simply copy the replica groups from the old ranges to the new. + * e.g.: + * We start with a portion of the ring containing tokens 100, 200 assigned to nodes A & B respectively. The node + * with the next highest token after B is C. RF is 2. We want to add a new token, 150 and assign it to a new node, D. + * Starting placement, with tokens 100, 200 + * (0, 100] : {A,B} + * (100, 200] : {B,C} + * Next placement, with tokens 100, 150, 200 but no ownership changes + * (0, 100] : {A,B} + * (100, 150] : {B,C} + * (150, 200] : {B,C} + * No actual movement has occurred, only the ranges have been adjusted. + * We then calculate the desired end state, with new nodes taking ownership of their assigned tokens. + * If Node D takes ownership of Token 150, the desired end state would be: + * (0, 100] : {A,D} + * (100, 150] : {D,C} + * (150, 200] : {B,C} + * As mentioned, distinct placements are maintained for reads and writes, enabling ranges to be temporarily + * overreplicated during a movement operation (analogous to the previous concept of pending ranges). + */ +public class UniformRangePlacement implements PlacementProvider +{ + private static final Logger logger = LoggerFactory.getLogger(UniformRangePlacement.class); + + @Override + public PlacementTransitionPlan planForJoin(ClusterMetadata metadata, + NodeId joining, + Set tokens, + Keyspaces keyspaces) + { + // There are no other nodes in the cluster, so the joining node will be taking ownership of the entire range. + if (metadata.tokenMap.isEmpty()) + { + DataPlacements placements = calculatePlacements(metadata.nextEpoch(), + metadata.transformer() + .proposeToken(joining, tokens) + .addToRackAndDC(joining) + .build() + .metadata, + keyspaces); + PlacementDeltas.Builder toStart = PlacementDeltas.builder(placements.size()); + placements.withDistributed((params, placement) -> { + toStart.put(params, DataPlacement.empty().difference(placements.get(params))); + }); + return new PlacementTransitionPlan(toStart.build(), + PlacementDeltas.empty(), + PlacementDeltas.empty(), + PlacementDeltas.empty()); + } + + DataPlacements base = calculatePlacements(metadata.nextEpoch(), metadata, keyspaces); + DataPlacements start = splitRanges(metadata.tokenMap, metadata.tokenMap.assignTokens(joining, tokens), base); + DataPlacements finalPlacements = calculatePlacements(metadata.nextEpoch(), + metadata.transformer() + .proposeToken(joining, tokens) + .addToRackAndDC(joining) + .build() + .metadata, + keyspaces); + DataPlacements maximalPlacements = start.combineReplicaGroups(finalPlacements); + PlacementDeltas.Builder toStart = PlacementDeltas.builder(base.size()); + PlacementDeltas.Builder toMaximal = PlacementDeltas.builder(base.size()); + PlacementDeltas.Builder toFinal = PlacementDeltas.builder(base.size()); + + start.withDistributed((params, startPlacement) -> { + toStart.put(params, base.get(params).difference(startPlacement)); + toMaximal.put(params, startPlacement.difference(maximalPlacements.get(params))); + toFinal.put(params, maximalPlacements.get(params).difference(finalPlacements.get(params))); + }); + // double check that the deltas make sense + PlacementTransitionPlan plan = new PlacementTransitionPlan(toStart.build(), toMaximal.build(), toFinal.build(), PlacementDeltas.empty()); + DataPlacements afterExecution = base.applyDelta(metadata.nextEpoch(), plan.toSplit) + .applyDelta(metadata.nextEpoch(), plan.addToWrites()) + .applyDelta(metadata.nextEpoch(), plan.moveReads()) + .applyDelta(metadata.nextEpoch(), plan.removeFromWrites()); + assertDiff(afterExecution, finalPlacements); + return plan; + } + + void assertDiff(DataPlacements calculated, DataPlacements applied) + { + calculated.forEach((params, placement) -> { + PlacementDeltas.PlacementDelta delta = placement.difference(applied.get(params)); + assert delta.writes.removals.isEmpty() : delta; + assert delta.writes.additions.isEmpty() : delta; + assert delta.reads.removals.isEmpty() : delta; + assert delta.reads.additions.isEmpty() : delta; + }); + } + + @Override + public PlacementTransitionPlan planForMove(ClusterMetadata metadata, NodeId nodeId, Set tokens, Keyspaces keyspaces) + { + DataPlacements base = calculatePlacements(metadata.nextEpoch(), metadata, keyspaces); + + TokenMap withMoved = metadata.tokenMap.assignTokens(nodeId, tokens); + // Introduce new tokens, but do not change the ownership just yet + DataPlacements start = splitRanges(metadata.tokenMap, withMoved, base); + + DataPlacements afterMove = calculatePlacements(metadata.nextEpoch(), + metadata.transformer() + .moveTokens(nodeId, tokens) + .build().metadata, + keyspaces); + + // Old tokens owned by the move target are now owned by the next-in-ring; + // Move target now owns its newly assigned tokens + DataPlacements end = splitRanges(metadata.tokenMap, withMoved, afterMove); + + DataPlacements maximal = start.combineReplicaGroups(end); + + PlacementDeltas.Builder split = PlacementDeltas.builder(); + PlacementDeltas.Builder toMaximal = PlacementDeltas.builder(); + PlacementDeltas.Builder toFinal = PlacementDeltas.builder(); + PlacementDeltas.Builder merge = PlacementDeltas.builder(); + + base.withDistributed((params, placement) -> { + split.put(params, placement.difference(start.get(params))); + }); + + maximal.withDistributed((params, placement) -> { + toMaximal.put(params, start.get(params).difference(placement)); + toFinal.put(params, placement.difference(end.get(params))); + }); + + end.withDistributed((params, placement) -> { + merge.put(params, placement.difference(afterMove.get(params))); + }); + + return new PlacementTransitionPlan(split.build(), toMaximal.build(), toFinal.build(), merge.build()); + } + + @Override + public PlacementTransitionPlan planForDecommission(ClusterMetadata metadata, + NodeId nodeId, + Keyspaces keyspaces) + { + // Determine the set of placements to start from. This is the canonical set of placements based on the current + // TokenMap and collection of Keyspaces/ReplicationParams. + List> currentRanges = calculateRanges(metadata.tokenMap); + DataPlacements start = calculatePlacements(metadata.nextEpoch(), currentRanges, metadata, keyspaces); + + ClusterMetadata proposed = metadata.transformer() + .unproposeTokens(nodeId) + .build() + .metadata; + DataPlacements withoutLeaving = calculatePlacements(metadata.nextEpoch(), proposed, keyspaces); + + DataPlacements finalPlacement = splitRanges(proposed.tokenMap, metadata.tokenMap, + withoutLeaving); + + DataPlacements maximal = start.combineReplicaGroups(finalPlacement); + + PlacementDeltas.Builder toMaximal = PlacementDeltas.builder(start.size()); + PlacementDeltas.Builder toFinal = PlacementDeltas.builder(start.size()); + PlacementDeltas.Builder merge = PlacementDeltas.builder(start.size()); + + maximal.withDistributed((params, maxPlacement) -> { + // Add new nodes as replicas + PlacementDeltas.PlacementDelta maxDelta = start.get(params).difference(maxPlacement); + toMaximal.put(params, maxDelta); + + PlacementDeltas.PlacementDelta leftDelta = maxPlacement.difference(finalPlacement.get(params)); + toFinal.put(params, leftDelta); + + PlacementDeltas.PlacementDelta finalDelta = finalPlacement.get(params).difference(withoutLeaving.get(params)); + merge.put(params, finalDelta); + }); + + return new PlacementTransitionPlan(PlacementDeltas.empty(), toMaximal.build(), toFinal.build(), merge.build()); + } + + @Override + public PlacementTransitionPlan planForReplacement(ClusterMetadata metadata, + NodeId replaced, + NodeId replacement, + Keyspaces keyspaces) + { + DataPlacements startPlacements = calculatePlacements(metadata.nextEpoch(), metadata, keyspaces); + DataPlacements finalPlacements = calculatePlacements(metadata.nextEpoch(), + metadata.transformer() + .replaced(replaced, replacement) + .build() + .metadata, + keyspaces); + DataPlacements maximalPlacements = startPlacements.combineReplicaGroups(finalPlacements); + + PlacementDeltas.Builder toMaximal = PlacementDeltas.builder(startPlacements.size()); + PlacementDeltas.Builder toFinal = PlacementDeltas.builder(startPlacements.size()); + + maximalPlacements.withDistributed((params, max) -> { + toMaximal.put(params, startPlacements.get(params).difference(max)); + toFinal.put(params, max.difference(finalPlacements.get(params))); + }); + + return new PlacementTransitionPlan(PlacementDeltas.empty(), toMaximal.build(), toFinal.build(), PlacementDeltas.empty()); + } + + public DataPlacements splitRanges(TokenMap current, + TokenMap proposed, + DataPlacements currentPlacements) + { + ImmutableList currentTokens = current.tokens(); + ImmutableList proposedTokens = proposed.tokens(); + if (currentTokens.isEmpty() || currentTokens.equals(proposedTokens)) + { + return currentPlacements; + } + else + { + if (!proposedTokens.containsAll(currentTokens)) + throw new IllegalArgumentException("Proposed tokens must be superset of existing tokens"); + // we need to split some existing ranges, so apply the new set of tokens to the current canonical + // placements to get a set of placements with the proposed ranges but the current replicas + return splitRangesForAllPlacements(proposedTokens, currentPlacements); + } + } + + @VisibleForTesting + DataPlacements splitRangesForAllPlacements(List proposedTokens, DataPlacements current) + { + DataPlacements.Builder builder = DataPlacements.builder(current.size()); + current.asMap().forEach((params, placement) -> { + // Don't split ranges for local-only placements + if (params.isLocal() || params.isMeta()) + builder.with(params, placement); + else + builder.with(params, placement.splitRangesForPlacement(proposedTokens)); + }); + return builder.build(); + } + + public DataPlacements calculatePlacements(Epoch epoch, ClusterMetadata metadata, Keyspaces keyspaces) + { + if (metadata.tokenMap.tokens().isEmpty()) + return DataPlacements.empty(); + + return calculatePlacements(epoch, keyspaces, metadata); + } + + private DataPlacements calculatePlacements(Epoch epoch, Keyspaces keyspaces, ClusterMetadata metadata) + { + List> ranges = calculateRanges(metadata.tokenMap); + return calculatePlacements(epoch, ranges, metadata, keyspaces); + } + + public List> calculateRanges(TokenMap tokenMap) + { + List tokens = tokenMap.tokens(); + List> ranges = new ArrayList<>(tokens.size() + 1); + maybeAdd(ranges, new Range<>(tokenMap.partitioner().getMinimumToken(), tokens.get(0))); + for (int i = 1; i < tokens.size(); i++) + maybeAdd(ranges, new Range<>(tokens.get(i - 1), tokens.get(i))); + maybeAdd(ranges, new Range<>(tokens.get(tokens.size() - 1), tokenMap.partitioner().getMinimumToken())); + if (ranges.isEmpty()) + ranges.add(new Range<>(tokenMap.partitioner().getMinimumToken(), tokenMap.partitioner().getMinimumToken())); + return ranges; + } + + @Override + public DataPlacements calculatePlacements(Epoch epoch, + List> ranges, + ClusterMetadata metadata, + Keyspaces keyspaces) + { + Map placements = new HashMap<>(); + for (KeyspaceMetadata ksMetadata : keyspaces) + { + logger.trace("Calculating data placements for {}", ksMetadata.name); + AbstractReplicationStrategy replication = ksMetadata.replicationStrategy; + ReplicationParams params = ksMetadata.params.replication; + if (params.isMeta() || params.isLocal()) + { + placements.put(params, metadata.placements.get(params)); + } + else + { + placements.computeIfAbsent(params, p -> replication.calculateDataPlacement(epoch, ranges, metadata)); + } + } + + return DataPlacements.builder(placements).build(); + } + + private void maybeAdd(List> ranges, Range r) + { + if (r.left.compareTo(r.right) != 0) + ranges.add(r); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/ownership/VersionedEndpoints.java b/src/java/org/apache/cassandra/tcm/ownership/VersionedEndpoints.java new file mode 100644 index 0000000000..c867b33f86 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/ownership/VersionedEndpoints.java @@ -0,0 +1,210 @@ +/* + * 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.tcm.ownership; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.Endpoints; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; + +/** + * Wrapper classes for EndpointsFor{Token|Range}, adding an indication of the last epoch when the placements were last modified. + */ +public interface VersionedEndpoints> extends MetadataValue>, Supplier +{ + static ForRange forRange(Epoch lastModified, EndpointsForRange endpointsForRange) + { + return new ForRange(lastModified, endpointsForRange); + } + + static ForToken forToken(Epoch lastModified, EndpointsForToken endpointsForToken) + { + return new ForToken(lastModified, endpointsForToken); + } + + class ForRange implements VersionedEndpoints + { + private final Epoch lastModified; + private final EndpointsForRange endpointsForRange; + + private ForRange(Epoch lastModified, EndpointsForRange endpointsForRange) + { + this.lastModified = lastModified; + this.endpointsForRange = endpointsForRange; + } + + public ForRange withLastModified(Epoch epoch) + { + return new ForRange(lastModified, endpointsForRange); + } + + public Epoch lastModified() + { + return lastModified; + } + + public EndpointsForRange get() + { + return endpointsForRange; + } + + public int size() + { + return endpointsForRange.size(); + } + + public final void forEach(Consumer forEach) + { + endpointsForRange.forEach(forEach); + } + + public Map byEndpoint() + { + return endpointsForRange.byEndpoint(); + } + + public Range range() + { + return endpointsForRange.range(); + } + + public Set endpoints() + { + return endpointsForRange.endpoints(); + } + + public ForRange map(Function fn) + { + return new ForRange(lastModified, fn.apply(endpointsForRange)); + } + + public ForToken forToken(Token token) + { + return new ForToken(lastModified, endpointsForRange.forToken(token)); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ForRange forRange = (ForRange) o; + return Objects.equals(endpointsForRange.sorted(Replica::compareTo), forRange.endpointsForRange.sorted(Replica::compareTo)); + } + + public boolean isEmpty() + { + return endpointsForRange.isEmpty(); + } + + public int hashCode() + { + return Objects.hash(endpointsForRange); + } + + public String toString() + { + return "ForRange{" + + "lastModified=" + lastModified + + ", endpointsForRange=" + endpointsForRange + + '}'; + } + } + + class ForToken implements VersionedEndpoints + { + private final Epoch lastModified; + private final EndpointsForToken endpointsForToken; + + private ForToken(Epoch lastModified, EndpointsForToken endpointsForRange) + { + this.lastModified = lastModified; + this.endpointsForToken = endpointsForRange; + } + + public ForToken withLastModified(Epoch epoch) + { + return new ForToken(lastModified, endpointsForToken); + } + + public ForToken map(Function fn) + { + return new ForToken(lastModified, fn.apply(endpointsForToken)); + } + + + public ForToken without(Set remove) + { + return map(e -> e.without(remove)); + } + + + public Epoch lastModified() + { + return lastModified; + } + + public EndpointsForToken get() + { + return endpointsForToken; + } + + public int size() + { + return endpointsForToken.size(); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ForToken forToken = (ForToken) o; + return Objects.equals(endpointsForToken, forToken.endpointsForToken); + } + + public boolean isEmpty() + { + return endpointsForToken.isEmpty(); + } + + public int hashCode() + { + return Objects.hash(endpointsForToken); + } + + public String toString() + { + return "ForToken{" + + "lastModified=" + lastModified + + ", endpointsForToken=" + endpointsForToken + + '}'; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/AddToCMS.java b/src/java/org/apache/cassandra/tcm/sequences/AddToCMS.java new file mode 100644 index 0000000000..f0a2e6f6c8 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/AddToCMS.java @@ -0,0 +1,208 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.HashSet; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.cms.FinishAddToCMS; +import org.apache.cassandra.tcm.transformations.cms.StartAddToCMS; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.TypeSizes.sizeof; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.JOIN_OWNERSHIP_GROUP; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; + +/** + * Add this or another node as a member of CMS. + * This sequence is unlike other implementations in that it has no 'prepare' phase. The real first step of the + * logical sequence, implemented by StartAddToCMS, is always executed first before the AddToCMS sequence itself + * is constructed. In fact, StartAddToCMS could be considered to perform the 'prepare' phase as well as the + * first step, which is to add the candidate as a write-only member of the CMS. Only once this has succeeded + * does the sequence start in earnest, performing streaming for the global log tables before executing the + * second and final logical step of promoting the candidate to a full read/write member of the CMS. + * + * This class along with AddToCMS & RemoveFromCMS, contain a high degree of duplication with their intended + * replacements ReconfigureCMS and AdvanceCMSReconfiguration. This shouldn't be a big problem as the intention is to + * remove this superceded version asap. + * @deprecated in favour of ReconfigureCMS + */ +@Deprecated(since = "CEP-21") +public class AddToCMS extends MultiStepOperation +{ + private static final Logger logger = LoggerFactory.getLogger(AddToCMS.class); + public static Serializer serializer = new Serializer(); + + private final NodeId toAdd; + private final Set streamCandidates; + private final FinishAddToCMS finishJoin; + + public static void initiate() + { + initiate(ClusterMetadata.current().myNodeId(), FBUtilities.getBroadcastAddressAndPort()); + } + + public static void initiate(NodeId nodeId, InetAddressAndPort addr) + { + MultiStepOperation sequence = ClusterMetadataService.instance() + .commit(new StartAddToCMS(addr)) + .inProgressSequences.get(nodeId); + InProgressSequences.resume(sequence); + ReconfigureCMS.repairPaxosTopology(); + } + + public AddToCMS(Epoch latestModification, + NodeId toAdd, + Set streamCandidates, + FinishAddToCMS join) + { + super(0, latestModification); + this.toAdd = toAdd; + this.streamCandidates = streamCandidates; + this.finishJoin = join; + } + + @Override + public Kind kind() + { + return JOIN_OWNERSHIP_GROUP; + } + + @Override + protected SequenceKey sequenceKey() + { + return toAdd; + } + + @Override + public MetadataSerializer keySerializer() + { + return NodeId.serializer; + } + + public Transformation.Kind nextStep() + { + return Transformation.Kind.FINISH_ADD_TO_CMS; + } + + @Override + public SequenceState executeNext() + { + try + { + ReconfigureCMS.streamRanges(finishJoin.replicaForStreaming(), streamCandidates); + ClusterMetadataService.instance().commit(finishJoin); + } + catch (Throwable t) + { + logger.error("Could not finish adding the node to the metadata ownership group", t); + } + + return continuable(); + } + + @Override + public AddToCMS advance(Epoch waitForWatermark) + { + throw new NoSuchElementException(); + } + + @Override + public ProgressBarrier barrier() + { + return ProgressBarrier.immediate(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AddToCMS addMember = (AddToCMS) o; + return Objects.equals(latestModification, addMember.latestModification) && + Objects.equals(streamCandidates, addMember.streamCandidates) && + Objects.equals(finishJoin, addMember.finishJoin); + } + + @Override + public int hashCode() + { + return Objects.hash(latestModification, streamCandidates, finishJoin); + } + + public static class Serializer implements AsymmetricMetadataSerializer, AddToCMS> + { + @Override + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + AddToCMS seq = (AddToCMS) t; + NodeId.serializer.serialize(seq.toAdd,out, version); + Epoch.serializer.serialize(seq.latestModification, out, version); + FinishAddToCMS.serializer.serialize(seq.finishJoin, out, version); + out.writeInt(seq.streamCandidates.size()); + for (InetAddressAndPort ep : seq.streamCandidates) + InetAddressAndPort.MetadataSerializer.serializer.serialize(ep, out, version); + } + + @Override + public AddToCMS deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + Epoch barrier = Epoch.serializer.deserialize(in, version); + FinishAddToCMS finish = FinishAddToCMS.serializer.deserialize(in, version); + int streamCandidatesSize = in.readInt(); + Set streamCandidates = new HashSet<>(); + + for (int i = 0; i < streamCandidatesSize; i++) + streamCandidates.add(InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version)); + return new AddToCMS(barrier, nodeId, streamCandidates, finish); + } + + @Override + public long serializedSize(MultiStepOperation t, Version version) + { + AddToCMS seq = (AddToCMS) t; + long size = NodeId.serializer.serializedSize(seq.toAdd, version); + size += Epoch.serializer.serializedSize(seq.latestModification, version); + size += FinishAddToCMS.serializer.serializedSize(seq.finishJoin, version); + size += sizeof(seq.streamCandidates.size()); + for (InetAddressAndPort ep : seq.streamCandidates) + size += InetAddressAndPort.MetadataSerializer.serializer.serializedSize(ep, version); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndJoin.java b/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndJoin.java new file mode 100644 index 0000000000..bd8d752316 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndJoin.java @@ -0,0 +1,523 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.stream.StreamSupport; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.googlecode.concurrenttrees.common.Iterables; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.StreamState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_JOIN; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_JOIN; +import static org.apache.cassandra.tcm.Transformation.Kind.START_JOIN; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.JOIN; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; +import static org.apache.cassandra.tcm.sequences.SequenceState.error; +import static org.apache.cassandra.tcm.sequences.SequenceState.halted; + +public class BootstrapAndJoin extends MultiStepOperation +{ + private static final Logger logger = LoggerFactory.getLogger(BootstrapAndJoin.class); + public static final Serializer serializer = new Serializer(); + + public final LockedRanges.Key lockKey; + public final PlacementDeltas toSplitRanges; + public final PrepareJoin.StartJoin startJoin; + public final PrepareJoin.MidJoin midJoin; + public final PrepareJoin.FinishJoin finishJoin; + public final Transformation.Kind next; + + public final boolean finishJoiningRing; + public final boolean streamData; + + public static BootstrapAndJoin newSequence(Epoch preparedAt, + LockedRanges.Key lockKey, + PlacementDeltas toSplitRanges, + PrepareJoin.StartJoin startJoin, + PrepareJoin.MidJoin midJoin, + PrepareJoin.FinishJoin finishJoin, + boolean finishJoiningRing, + boolean streamData) + { + return new BootstrapAndJoin(preparedAt, + lockKey, + toSplitRanges, + START_JOIN, + startJoin, midJoin, finishJoin, + finishJoiningRing, streamData); + } + + /** + * Used by factory method for external callers and by Serializer + */ + @VisibleForTesting + BootstrapAndJoin(Epoch latestModification, + LockedRanges.Key lockKey, + PlacementDeltas toSplitRanges, + Transformation.Kind next, + PrepareJoin.StartJoin startJoin, + PrepareJoin.MidJoin midJoin, + PrepareJoin.FinishJoin finishJoin, + boolean finishJoiningRing, + boolean streamData) + { + super(nextToIndex(next), latestModification); + this.lockKey = lockKey; + this.toSplitRanges = toSplitRanges; + this.next = next; + this.startJoin = startJoin; + this.midJoin = midJoin; + this.finishJoin = finishJoin; + this.finishJoiningRing = finishJoiningRing; + this.streamData = streamData; + } + + /** + * Used by advance to move forward in the sequence after execution + */ + private BootstrapAndJoin(BootstrapAndJoin current, Epoch latestModification) + { + super(current.idx + 1, latestModification); + this.next = indexToNext(current.idx + 1); + this.lockKey = current.lockKey; + this.toSplitRanges = current.toSplitRanges; + this.startJoin = current.startJoin; + this.midJoin = current.midJoin; + this.finishJoin = current.finishJoin; + this.finishJoiningRing = current.finishJoiningRing; + this.streamData = current.streamData; + } + + @Override + public Kind kind() + { + return JOIN; + } + + @Override + protected SequenceKey sequenceKey() + { + return startJoin.nodeId(); + } + + @Override + public MetadataSerializer keySerializer() + { + return NodeId.serializer; + } + + @Override + public Transformation.Kind nextStep() + { + return indexToNext(idx); + } + + @Override + public SequenceState executeNext() + { + switch (next) + { + case START_JOIN: + try + { + SystemKeyspace.updateLocalTokens(finishJoin.tokens); + ClusterMetadataService.instance().commit(startJoin); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.warn("Exception committing startJoin", e); + return continuable(); + } + + break; + case MID_JOIN: + try + { + Collection bootstrapTokens = SystemKeyspace.getSavedTokens(); + ClusterMetadata metadata = ClusterMetadata.current(); + Pair movements = getMovementMaps(metadata); + MovementMap movementMap = movements.left; + MovementMap strictMovementMap = movements.right; + if (streamData) + { + boolean dataAvailable = bootstrap(bootstrapTokens, + StorageService.INDEFINITE, + ClusterMetadata.current(), + null, + movementMap, + strictMovementMap); + + if (!dataAvailable) + { + logger.warn("Some data streaming failed. Use nodetool to check bootstrap state and resume. " + + "For more, see `nodetool help bootstrap`. {}", SystemKeyspace.getBootstrapState()); + return halted(); + } + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + } + else + { + // The node may have previously been started in write survey mode and may or may not have + // performed initial streaming (i.e. auto_bootstap: false). When an operator then manually joins + // it (or it bounces and comes up without the system property), it will hit this condition. + // If during the initial startup no streaming was performed then bootstrap state is not + // COMPLETED and so we log the message about skipping data streaming. Alternatively, if + // streaming was done before entering write survey mode, the bootstrap is COMPLETE and so no + // need to log. + // The ability to join without bootstrapping, especially when combined with write survey mode + // is probably a mis-feature and serious consideration should be given to removing it. + if (!SystemKeyspace.bootstrapComplete()) + logger.info("Skipping data streaming for join"); + } + + if (finishJoiningRing) + { + StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false) + .filter(cfs -> Schema.instance.getUserKeyspaces().names().contains(cfs.keyspace.getName())) + .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(true)); + ClusterMetadataService.instance().commit(midJoin); + } + else + { + logger.info("Startup complete, but write survey mode is active, not becoming an active ring member. Use JMX (StorageService->joinRing()) to finalize ring joining."); + return halted(); + } + } + catch (IllegalStateException e) + { + logger.error("Can't complete bootstrap", e); + return error(e); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.info("Exception committing midJoin", e); + return halted(); + } + + break; + case FINISH_JOIN: + try + { + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + ClusterMetadataService.instance().commit(finishJoin); + StorageService.instance.clearTransientMode(); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.warn("Exception committing finishJoin", e); + return continuable(); + } + break; + default: + return error(new IllegalStateException("Can't proceed with join from " + next)); + } + return continuable(); + } + + @Override + public BootstrapAndJoin advance(Epoch waitForWatermark) + { + return new BootstrapAndJoin(this, waitForWatermark); + } + + @Override + public ProgressBarrier barrier() + { + // There is no requirement to wait for peers to sync before starting the sequence + if (next == START_JOIN) + return ProgressBarrier.immediate(); + ClusterMetadata metadata = ClusterMetadata.current(); + return new ProgressBarrier(latestModification, metadata.directory.location(startJoin.nodeId()), metadata.lockedRanges.locked.get(lockKey)); + } + + @Override + public ClusterMetadata.Transformer cancel(ClusterMetadata metadata) + { + DataPlacements placements = metadata.placements; + switch (next) + { + // need to undo MID_JOIN and START_JOIN, then merge the ranges split by PrepareJoin + case FINISH_JOIN: + placements = midJoin.inverseDelta().apply(metadata.nextEpoch(), placements); + case MID_JOIN: + placements = startJoin.inverseDelta().apply(metadata.nextEpoch(), placements); + case START_JOIN: + placements = toSplitRanges.invert().apply(metadata.nextEpoch(), placements); + break; + default: + throw new IllegalStateException("Can't revert join from " + next); + } + LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey); + return metadata.transformer() + .withNodeState(startJoin.nodeId(), NodeState.REGISTERED) + .with(placements) + .with(newLockedRanges); + } + + public BootstrapAndJoin finishJoiningRing() + { + return new BootstrapAndJoin(latestModification, lockKey, toSplitRanges, + next, startJoin, midJoin, finishJoin, + true, false); + } + + @VisibleForTesting + public Pair getMovementMaps(ClusterMetadata metadata) + { + MovementMap movementMap = movementMap(metadata.directory.endpoint(startJoin.nodeId()), metadata.placements, startJoin.delta()); + MovementMap strictMovementMap = toStrict(movementMap, finishJoin.delta()); + return Pair.create(movementMap, strictMovementMap); + } + + // TODO this is reused by BootstrapAndReplace, should we move it somewhere common? + public static boolean bootstrap(final Collection tokens, + long bootstrapTimeoutMillis, + ClusterMetadata metadata, + InetAddressAndPort beingReplaced, + MovementMap movements, + MovementMap strictMovements) + { + SystemKeyspace.updateLocalTokens(tokens); + assert beingReplaced == null || strictMovements == null : "Can't have strict movements during replacements"; + + if (CassandraRelevantProperties.RESET_BOOTSTRAP_PROGRESS.getBoolean()) + { + logger.info("Resetting bootstrap progress to start fresh"); + SystemKeyspace.resetAvailableStreamedRanges(); + } + Future bootstrapStream = StorageService.instance.startBootstrap(metadata, beingReplaced, movements, strictMovements); + try + { + if (bootstrapTimeoutMillis > 0) + bootstrapStream.get(bootstrapTimeoutMillis, MILLISECONDS); + else + bootstrapStream.get(); + StorageService.instance.markViewsAsBuilt(); + StorageService.instance.clearOngoingBootstrap(); + logger.info("Bootstrap completed for tokens {}", tokens); + return true; + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.error("Error while waiting on bootstrap to complete. Bootstrap will have to be restarted.", e); + return false; + } + } + + private static MovementMap movementMap(InetAddressAndPort joining, DataPlacements placements, PlacementDeltas startDelta) + { + MovementMap.Builder movementMapBuilder = MovementMap.builder(); + // we need all original placements for the ranges to stream - after initial split these new ranges exist in placements + // startDelta write additions contains the ranges we need to stream + startDelta.forEach((params, delta) -> { + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + DataPlacement oldPlacement = placements.get(params); + delta.writes.additions.flattenValues().forEach((destination) -> { + assert destination.endpoint().equals(joining); + oldPlacement.reads.forRange(destination.range()) + .get() + .stream() + .forEach(source -> movements.put(destination, source)); + }); + movementMapBuilder.put(params, movements.build()); + }); + return movementMapBuilder.build(); + } + + private static MovementMap toStrict(MovementMap completeMovementMap, PlacementDeltas finishDelta) + { + MovementMap.Builder movementMapBuilder = MovementMap.builder(); + completeMovementMap.forEach((params, byreplica) -> { + Set strictCandidates = Iterables.toSet(finishDelta.get(params).writes.removals.flattenValues()); + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + for (Replica destination : byreplica.keySet()) + { + byreplica.get(destination).forEach((source) -> { + if (strictCandidates.contains(source)) + movements.put(destination, source); + }); + } + movementMapBuilder.put(params, movements.build()); + }); + return movementMapBuilder.build(); + } + + private static int nextToIndex(Transformation.Kind next) + { + switch (next) + { + case START_JOIN: + return 0; + case MID_JOIN: + return 1; + case FINISH_JOIN: + return 2; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", next, JOIN)); + } + } + + private static Transformation.Kind indexToNext(int index) + { + switch (index) + { + case 0: + return START_JOIN; + case 1: + return MID_JOIN; + case 2: + return FINISH_JOIN; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", index, JOIN)); + } + } + + public String toString() + { + return "BootstrapAndJoinPlan{" + + "barrier=" + latestModification + + ", lockKey=" + lockKey + + ", toSplitRanges=" + toSplitRanges + + ", startJoin=" + startJoin + + ", midJoin=" + midJoin + + ", finishJoin=" + finishJoin + + ", next=" + next + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BootstrapAndJoin that = (BootstrapAndJoin) o; + return finishJoiningRing == that.finishJoiningRing && + streamData == that.streamData && + next == that.next && + Objects.equals(latestModification, that.latestModification) && + Objects.equals(lockKey, that.lockKey) && + Objects.equals(toSplitRanges, that.toSplitRanges) && + Objects.equals(startJoin, that.startJoin) && + Objects.equals(midJoin, that.midJoin) && + Objects.equals(finishJoin, that.finishJoin); + } + + @Override + public int hashCode() + { + return Objects.hash(latestModification, lockKey, toSplitRanges, startJoin, midJoin, finishJoin, next, finishJoiningRing, streamData); + } + + public static class Serializer implements AsymmetricMetadataSerializer, BootstrapAndJoin> + { + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + BootstrapAndJoin plan = (BootstrapAndJoin) t; + out.writeBoolean(plan.finishJoiningRing); + out.writeBoolean(plan.streamData); + + Epoch.serializer.serialize(plan.latestModification, out, version); + LockedRanges.Key.serializer.serialize(plan.lockKey, out, version); + PlacementDeltas.serializer.serialize(plan.toSplitRanges, out, version); + VIntCoding.writeUnsignedVInt32(plan.next.ordinal(), out); + + PrepareJoin.StartJoin.serializer.serialize(plan.startJoin, out, version); + PrepareJoin.MidJoin.serializer.serialize(plan.midJoin, out, version); + PrepareJoin.FinishJoin.serializer.serialize(plan.finishJoin, out, version); + } + + public BootstrapAndJoin deserialize(DataInputPlus in, Version version) throws IOException + { + boolean finishJoiningRing = in.readBoolean(); + boolean streamData = in.readBoolean(); + + Epoch lastModified = Epoch.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + PlacementDeltas toSplitRanges = PlacementDeltas.serializer.deserialize(in, version); + Transformation.Kind next = Transformation.Kind.values()[VIntCoding.readUnsignedVInt32(in)]; + PrepareJoin.StartJoin startJoin = PrepareJoin.StartJoin.serializer.deserialize(in, version); + PrepareJoin.MidJoin midJoin = PrepareJoin.MidJoin.serializer.deserialize(in, version); + PrepareJoin.FinishJoin finishJoin = PrepareJoin.FinishJoin.serializer.deserialize(in, version); + + return new BootstrapAndJoin(lastModified, lockKey, toSplitRanges, next, startJoin, midJoin, finishJoin, finishJoiningRing, streamData); + } + + public long serializedSize(MultiStepOperation t, Version version) + { + BootstrapAndJoin plan = (BootstrapAndJoin) t; + long size = (TypeSizes.BOOL_SIZE * 2); + + size += Epoch.serializer.serializedSize(plan.latestModification, version); + size += LockedRanges.Key.serializer.serializedSize(plan.lockKey, version); + size += PlacementDeltas.serializer.serializedSize(plan.toSplitRanges, version); + + size += VIntCoding.computeVIntSize(plan.kind().ordinal()); + + size += PrepareJoin.StartJoin.serializer.serializedSize(plan.startJoin, version); + size += PrepareJoin.MidJoin.serializer.serializedSize(plan.midJoin, version); + size += PrepareJoin.FinishJoin.serializer.serializedSize(plan.finishJoin, version); + + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java b/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java new file mode 100644 index 0000000000..60d9ff35d1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java @@ -0,0 +1,500 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.StreamSupport; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_REPLACE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_REPLACE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_REPLACE; +import static org.apache.cassandra.tcm.sequences.BootstrapAndJoin.bootstrap; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.REPLACE; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; +import static org.apache.cassandra.tcm.sequences.SequenceState.error; +import static org.apache.cassandra.tcm.sequences.SequenceState.halted; + +public class BootstrapAndReplace extends MultiStepOperation +{ + private static final Logger logger = LoggerFactory.getLogger(BootstrapAndReplace.class); + public static final Serializer serializer = new Serializer(); + + public final LockedRanges.Key lockKey; + public final Set bootstrapTokens; + public final PrepareReplace.StartReplace startReplace; + public final PrepareReplace.MidReplace midReplace; + public final PrepareReplace.FinishReplace finishReplace; + public final Transformation.Kind next; + + public final boolean finishJoiningRing; + public final boolean streamData; + + public static BootstrapAndReplace newSequence(Epoch preparedAt, + LockedRanges.Key lockKey, + Set bootstrapTokens, + PrepareReplace.StartReplace startReplace, + PrepareReplace.MidReplace midReplace, + PrepareReplace.FinishReplace finishReplace, + boolean finishJoiningRing, + boolean streamData) + { + return new BootstrapAndReplace(preparedAt, + lockKey, + bootstrapTokens, + START_REPLACE, + startReplace, midReplace, finishReplace, + finishJoiningRing, streamData); + } + + /** + * Used by factory method for external callers and by Serializer + */ + @VisibleForTesting + BootstrapAndReplace(Epoch latestModification, + LockedRanges.Key lockKey, + Set bootstrapTokens, + Transformation.Kind next, + PrepareReplace.StartReplace startReplace, + PrepareReplace.MidReplace midReplace, + PrepareReplace.FinishReplace finishReplace, + boolean finishJoiningRing, + boolean streamData) + { + super(nextToIndex(next), latestModification); + this.lockKey = lockKey; + this.bootstrapTokens = bootstrapTokens; + this.next = next; + this.startReplace = startReplace; + this.midReplace = midReplace; + this.finishReplace = finishReplace; + this.finishJoiningRing = finishJoiningRing; + this.streamData = streamData; + } + + /** + * Used by advance to move forward in the sequence after execution + */ + private BootstrapAndReplace(BootstrapAndReplace current, Epoch latestModification) + { + super(current.idx + 1, latestModification); + this.next = indexToNext(current.idx + 1); + this.lockKey = current.lockKey; + this.bootstrapTokens = current.bootstrapTokens; + this.startReplace = current.startReplace; + this.midReplace = current.midReplace; + this.finishReplace = current.finishReplace; + this.finishJoiningRing = current.finishJoiningRing; + this.streamData = current.streamData; + } + + @Override + public Kind kind() + { + return REPLACE; + } + + @Override + protected SequenceKey sequenceKey() + { + return startReplace.nodeId(); + } + + @Override + public MetadataSerializer keySerializer() + { + return NodeId.serializer; + } + + @Override + public Transformation.Kind nextStep() + { + return indexToNext(idx); + } + + @Override + public SequenceState executeNext() + { + switch (next) + { + // Last transformation is register, so we move to PrepareReplace + case START_REPLACE: + try + { + ClusterMetadataService.instance().commit(startReplace); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.warn("Got exception committing startReplace", e); + return continuable(); + } + break; + case MID_REPLACE: + try + { + ClusterMetadata metadata = ClusterMetadata.current(); + + if (streamData) + { + MovementMap movements = movementMap(metadata.directory.endpoint(startReplace.replaced()), startReplace.delta()); + boolean dataAvailable = bootstrap(bootstrapTokens, + StorageService.INDEFINITE, + metadata, + metadata.directory.endpoint(startReplace.replaced()), + movements, + null); // no potential for strict movements when replacing + + if (!dataAvailable) + { + logger.warn("Some data streaming failed. Use nodetool to check bootstrap state and resume. " + + "For more, see `nodetool help bootstrap`. {}", SystemKeyspace.getBootstrapState()); + return halted(); + } + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + } + else + { + // The node may have previously been started in write survey mode and may or may not have + // performed initial streaming (i.e. auto_bootstap: false). When an operator then manually joins + // it (or it bounces and comes up without the system property), it will hit this condition. + // If during the initial startup no streaming was performed then bootstrap state is not + // COMPLETED and so we log the message about skipping data streaming. Alternatively, if + // streaming was done before entering write survey mode, the bootstrap is COMPLETE and so no + // need to log. + // The ability to join without bootstrapping, especially when combined with write survey mode + // is probably a mis-feature and serious consideration should be given to removing it. + if (!SystemKeyspace.bootstrapComplete()) + logger.info("Skipping data streaming for join"); + } + + if (finishJoiningRing) + { + StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false) + .filter(cfs -> Schema.instance.getUserKeyspaces().names().contains(cfs.keyspace.getName())) + .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(true)); + ClusterMetadataService.instance().commit(midReplace); + } + else + { + logger.info("Startup complete, but write survey mode is active, not becoming an active ring member. Use JMX (StorageService->joinRing()) to finalize ring joining."); + return halted(); + } + } + catch (IllegalStateException e) + { + logger.error("Can't complete replacement", e); + return error(e); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.warn("Got exception committing midReplace", e); + return halted(); + } + break; + case FINISH_REPLACE: + try + { + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + ClusterMetadataService.instance().commit(finishReplace); + } + catch (Throwable e) + { + JVMStabilityInspector.inspectThrowable(e); + logger.warn("Got exception committing finishReplace", e); + return halted(); + } + break; + default: + return error(new IllegalStateException("Can't proceed with replacement from " + next)); + } + + return continuable(); + } + + @Override + public BootstrapAndReplace advance(Epoch waitFor) + { + return new BootstrapAndReplace(this, waitFor); + } + + @Override + public ProgressBarrier barrier() + { + // There is no requirement to wait for peers to sync before starting the sequence + if (next == START_REPLACE) + return ProgressBarrier.immediate(); + ClusterMetadata metadata = ClusterMetadata.current(); + InetAddressAndPort replaced = metadata.directory.getNodeAddresses(startReplace.replaced()).broadcastAddress; + return new ProgressBarrier(latestModification, metadata.directory.location(startReplace.nodeId()), metadata.lockedRanges.locked.get(lockKey), e -> !e.equals(replaced)); + } + + @Override + public ClusterMetadata.Transformer cancel(ClusterMetadata metadata) + { + DataPlacements placements = metadata.placements; + switch (next) + { + // need to undo MID_REPLACE and START_REPLACE, but PREPARE_REPLACE doesn't affect placements + case FINISH_REPLACE: + placements = midReplace.inverseDelta().apply(metadata.nextEpoch(), placements); + case MID_REPLACE: + case START_REPLACE: + placements = startReplace.inverseDelta().apply(metadata.nextEpoch(), placements); + break; + default: + throw new IllegalStateException("Can't revert replacement from " + next); + } + + LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey); + return metadata.transformer() + .withNodeState(startReplace.replacement(), NodeState.REGISTERED) + .with(placements) + .with(newLockedRanges); + } + + public BootstrapAndReplace finishJoiningRing() + { + return new BootstrapAndReplace(latestModification, lockKey, bootstrapTokens, + next, startReplace, midReplace, finishReplace, + true, false); + } + + /** + * startDelta.writes.additions contains the ranges we need to stream + * for each of those ranges, add all possible endpoints (except for the replica we're replacing) to the movement map + * + * keys in the map are the ranges the replacement node needs to stream, values are the potential endpoints. + */ + private static MovementMap movementMap(InetAddressAndPort beingReplaced, PlacementDeltas startDelta) + { + MovementMap.Builder movementMapBuilder = MovementMap.builder(); + DataPlacements placements = ClusterMetadata.current().placements; + startDelta.forEach((params, delta) -> { + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + DataPlacement originalPlacements = placements.get(params); + delta.writes.additions.flattenValues().forEach((destination) -> { + originalPlacements.reads.forRange(destination.range()) + .get().stream() + .filter(r -> !r.endpoint().equals(beingReplaced)) + .forEach(source -> movements.put(destination, source)); + }); + movementMapBuilder.put(params, movements.build()); + }); + return movementMapBuilder.build(); + } + + private static int nextToIndex(Transformation.Kind next) + { + switch (next) + { + case START_REPLACE: + return 0; + case MID_REPLACE: + return 1; + case FINISH_REPLACE: + return 2; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", next, REPLACE)); + } + } + + private static Transformation.Kind indexToNext(int index) + { + switch (index) + { + case 0: + return START_REPLACE; + case 1: + return MID_REPLACE; + case 2: + return FINISH_REPLACE; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", index, REPLACE)); + } + } + + public String toString() + { + return "BootstrapAndReplacePlan{" + + "barrier=" + barrier() + + "lockKey=" + lockKey + + ", bootstrapTokens=" + bootstrapTokens + + ", startReplace=" + startReplace + + ", midReplace=" + midReplace + + ", finishReplace=" + finishReplace + + ", next=" + next + + ", finishJoiningRing=" + finishJoiningRing + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BootstrapAndReplace that = (BootstrapAndReplace) o; + return finishJoiningRing == that.finishJoiningRing && + streamData == that.streamData && + next == that.next && + Objects.equals(latestModification, that.latestModification) && + Objects.equals(lockKey, that.lockKey) && + Objects.equals(bootstrapTokens, that.bootstrapTokens) && + Objects.equals(startReplace, that.startReplace) && + Objects.equals(midReplace, that.midReplace) && + Objects.equals(finishReplace, that.finishReplace); + } + + @Override + public int hashCode() + { + return Objects.hash(latestModification, lockKey, bootstrapTokens, startReplace, midReplace, finishReplace, next, finishJoiningRing, streamData); + } + + public static void checkUnsafeReplace(boolean shouldBootstrap) + { + if (!shouldBootstrap && !CassandraRelevantProperties.ALLOW_UNSAFE_REPLACE.getBoolean()) + { + throw new RuntimeException("Replacing a node without bootstrapping risks invalidating consistency " + + "guarantees as the expected data may not be present until repair is run. " + + "To perform this operation, please restart with " + + "-Dcassandra.allow_unsafe_replace=true"); + } + + } + + public static void gossipStateToHibernate(ClusterMetadata metadata, NodeId nodeId) + { + // order is important here, the gossiper can fire in between adding these two states. It's ok to send TOKENS without STATUS, but *not* vice versa. + List> states = new ArrayList<>(); + VersionedValue.VersionedValueFactory valueFactory = StorageService.instance.valueFactory; + states.add(Pair.create(ApplicationState.TOKENS, valueFactory.tokens(metadata.tokenMap.tokens(nodeId)))); + states.add(Pair.create(ApplicationState.STATUS_WITH_PORT, valueFactory.hibernate(true))); + states.add(Pair.create(ApplicationState.STATUS, valueFactory.hibernate(true))); + Gossiper.instance.addLocalApplicationStates(states); + } + + public static class Serializer implements AsymmetricMetadataSerializer, BootstrapAndReplace> + { + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + BootstrapAndReplace plan = (BootstrapAndReplace) t; + out.writeBoolean(plan.finishJoiningRing); + out.writeBoolean(plan.streamData); + + out.writeUnsignedVInt32(plan.bootstrapTokens.size()); + for (Token token : plan.bootstrapTokens) + Token.metadataSerializer.serialize(token, out, version); + + Epoch.serializer.serialize(plan.latestModification, out, version); + LockedRanges.Key.serializer.serialize(plan.lockKey, out, version); + + VIntCoding.writeUnsignedVInt32(plan.next.ordinal(), out); + PrepareReplace.StartReplace.serializer.serialize(plan.startReplace, out, version); + PrepareReplace.MidReplace.serializer.serialize(plan.midReplace, out, version); + PrepareReplace.FinishReplace.serializer.serialize(plan.finishReplace, out, version); + } + + public BootstrapAndReplace deserialize(DataInputPlus in, Version version) throws IOException + { + boolean finishJoiningRing = in.readBoolean(); + boolean streamData = in.readBoolean(); + + Set tokens = new HashSet<>(); + int tokenCount = VIntCoding.readUnsignedVInt32(in); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < tokenCount; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + + Epoch barrier = Epoch.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + + Transformation.Kind next = Transformation.Kind.values()[VIntCoding.readUnsignedVInt32(in)]; + PrepareReplace.StartReplace startReplace = PrepareReplace.StartReplace.serializer.deserialize(in, version); + PrepareReplace.MidReplace midReplace = PrepareReplace.MidReplace.serializer.deserialize(in, version); + PrepareReplace.FinishReplace finishReplace = PrepareReplace.FinishReplace.serializer.deserialize(in, version); + + return new BootstrapAndReplace(barrier, lockKey, tokens, next, startReplace, midReplace, finishReplace, finishJoiningRing, streamData); + } + + public long serializedSize(MultiStepOperation t, Version version) + { + BootstrapAndReplace plan = (BootstrapAndReplace) t; + long size = (TypeSizes.BOOL_SIZE * 2); + + size += VIntCoding.computeVIntSize(plan.bootstrapTokens.size()); + for (Token token : plan.bootstrapTokens) + size += Token.metadataSerializer.serializedSize(token, version); + + size += Epoch.serializer.serializedSize(plan.latestModification, version); + size += LockedRanges.Key.serializer.serializedSize(plan.lockKey, version); + + size += VIntCoding.computeVIntSize(plan.kind().ordinal()); + + size += PrepareReplace.StartReplace.serializer.serializedSize(plan.startReplace, version); + size += PrepareReplace.MidReplace.serializer.serializedSize(plan.midReplace, version); + size += PrepareReplace.FinishReplace.serializer.serializedSize(plan.finishReplace, version); + + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/CancelCMSReconfiguration.java b/src/java/org/apache/cassandra/tcm/sequences/CancelCMSReconfiguration.java new file mode 100644 index 0000000000..b8a8b2c6af --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/CancelCMSReconfiguration.java @@ -0,0 +1,105 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; + +import org.apache.cassandra.exceptions.ExceptionCode; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +public class CancelCMSReconfiguration implements Transformation +{ + public static final Serializer serializer = new Serializer(); + + public static final CancelCMSReconfiguration instance = new CancelCMSReconfiguration(); + private CancelCMSReconfiguration() + { + } + + @Override + public Kind kind() + { + return Kind.CANCEL_CMS_RECONFIGURATION; + } + + @Override + public Result execute(ClusterMetadata prev) + { + ReconfigureCMS reconfigureCMS = (ReconfigureCMS) prev.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + if (reconfigureCMS == null) + return new Rejected(ExceptionCode.INVALID, "Can not cancel reconfiguration since there does not seem to be any in-flight"); + + ReplicationParams metaParams = ReplicationParams.meta(prev); + ClusterMetadata.Transformer transformer = prev.transformer(); + if (reconfigureCMS.next.activeTransition != null) + { + InetAddressAndPort pendingEndpoint = prev.directory.endpoint(reconfigureCMS.next.activeTransition.nodeId); + Replica pendingReplica = new Replica(pendingEndpoint, entireRange, true); + DataPlacement.Builder builder = prev.placements.get(metaParams).unbuild() + .withoutWriteReplica(prev.nextEpoch(), pendingReplica); + + DataPlacement placement = builder.build(); + if (!placement.reads.equals(placement.writes)) + return new Rejected(ExceptionCode.INVALID, String.format("Placements will be inconsistent if this transformation is applied:\nReads %s\nWrites: %s", + placement.reads, + placement.writes)); + + transformer = transformer.with(prev.placements.unbuild().with(metaParams, placement).build()); + } + + return Transformation.success(transformer.with(prev.inProgressSequences.without(ReconfigureCMS.SequenceKey.instance)) + .with(prev.lockedRanges.unlock(reconfigureCMS.next.lockKey)), + EntireRange.affectedRanges(prev)); + } + + @Override + public String toString() + { + return "CancelCMSReconfiguration{}"; + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + } + + public CancelCMSReconfiguration deserialize(DataInputPlus in, Version version) throws IOException + { + return instance; + } + + public long serializedSize(Transformation t, Version version) + { + return 0; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/DataMovements.java b/src/java/org/apache/cassandra/tcm/sequences/DataMovements.java new file mode 100644 index 0000000000..e9257acec1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/DataMovements.java @@ -0,0 +1,143 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.streaming.DataMovement; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.utils.concurrent.AsyncPromise; + +public class DataMovements implements IVerbHandler +{ + private static final Logger logger = LoggerFactory.getLogger(DataMovements.class); + public static final DataMovements instance = new DataMovements(); + + private final Map> inFlightMovements = new ConcurrentHashMap<>(); + + public ResponseTracker registerMovements(StreamOperation type, String operationId, MovementMap movements) + { + Map inFlightForType = inFlightMovements.computeIfAbsent(type, (type_) -> new ConcurrentHashMap<>()); + return inFlightForType.computeIfAbsent(operationId, (id) -> new ResponseTracker(movements)); + } + + public void unregisterMovements(StreamOperation type, String operationId) + { + Map inFlightForType = inFlightMovements.get(type); + if (inFlightForType != null) + inFlightForType.remove(operationId); + } + + @Override + public void doVerb(Message message) throws IOException + { + Map inFlightForType = inFlightMovements.get(StreamOperation.valueOf(message.payload.operationType)); + ResponseTracker responseTracker = inFlightForType.get(message.payload.operationId); + if (responseTracker != null) + { + if (message.payload.success) + responseTracker.received(message.from()); + else + responseTracker.failure(message.from()); + } + else + { + logger.error("Got DataMovement executed message for {}:{} from {}, but no tracker registered for that operation", + message.payload.operationType, message.payload.operationId, message.from()); + logger.debug("Current in-flight movements: {}", DataMovements.instance); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + inFlightMovements.forEach((type, byType) -> { + sb.append(type).append('['); + byType.forEach((id, tracker) -> { + sb.append(id).append(':'); + tracker.remaining().forEach(i -> sb.append(i.toString(true)).append(',')); + }); + sb.append("],"); + }); + return sb.toString(); + } + + public static class ResponseTracker + { + private final Set expected = ConcurrentHashMap.newKeySet(); + private final AsyncPromise promise = new AsyncPromise<>(); + + public ResponseTracker(MovementMap movements) + { + movements.byEndpoint().forEach((endpoint, epMovements) -> expected.addAll(epMovements.byEndpoint().keySet())); + if (expected.isEmpty()) + promise.setSuccess(null); + } + + public void received(InetAddressAndPort from) + { + logger.info("Received stream completion from {}", from); + expected.remove(from); + if (expected.isEmpty()) + promise.setSuccess(null); + } + + public void failure(InetAddressAndPort from) + { + logger.warn("Received stream failure from {}", from); + if (expected.contains(from)) + promise.setFailure(new RuntimeException()); + } + + public void await() + { + try + { + promise.get(); + } + catch (InterruptedException | ExecutionException e) + { + throw new RuntimeException(e); + } + } + + public Set remaining() + { + return new HashSet<>(expected); + } + + public String toString() + { + return expected.toString(); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/InProgressSequences.java b/src/java/org/apache/cassandra/tcm/sequences/InProgressSequences.java new file mode 100644 index 0000000000..a8b2b16259 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/InProgressSequences.java @@ -0,0 +1,304 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; + +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.db.TypeSizes.sizeof; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.LEAVE; +import static org.apache.cassandra.tcm.serialization.Version.V2; + +public class InProgressSequences implements MetadataValue +{ + public static final Serializer serializer = new Serializer(); + + public static InProgressSequences EMPTY = new InProgressSequences(Epoch.EMPTY, ImmutableMap.of()); + private final ImmutableMap> state; + private final Epoch lastModified; + + private InProgressSequences(Epoch lastModified, ImmutableMap> state) + { + this.lastModified = lastModified; + this.state = state; + } + + public static void finishInProgressSequences(MultiStepOperation.SequenceKey sequenceKey) + { + ClusterMetadata metadata = ClusterMetadata.current(); + while (true) + { + MultiStepOperation sequence = metadata.inProgressSequences.get(sequenceKey); + if (sequence == null) + break; + if (isLeave(sequence)) + StorageService.instance.maybeInitializeServices(); + if (resume(sequence)) + metadata = ClusterMetadata.current(); + else + return; + } + } + + public static boolean cancelInProgressSequences(String sequenceOwner, String expectedSequenceKind) + { + NodeId owner = NodeId.fromString(sequenceOwner); + MultiStepOperation seq = ClusterMetadata.current().inProgressSequences.get(owner); + if (seq == null) + throw new IllegalArgumentException("No in progress sequence for "+sequenceOwner); + MultiStepOperation.Kind expectedKind = MultiStepOperation.Kind.valueOf(expectedSequenceKind); + if (seq.kind() != expectedKind) + throw new IllegalArgumentException("No in progress sequence of kind " + expectedKind + " for " + owner + " (only " + seq.kind() +" in progress)"); + + return StorageService.cancelInProgressSequences(owner); + } + + @Override + public InProgressSequences withLastModified(Epoch epoch) + { + return new InProgressSequences(epoch, state); + } + + @Override + public Epoch lastModified() + { + return lastModified; + } + + public boolean contains(MultiStepOperation.SequenceKey key) + { + return state.containsKey(key); + } + + public MultiStepOperation get(MultiStepOperation.SequenceKey key) + { + return state.get(key); + } + + public boolean isEmpty() + { + return state.isEmpty(); + } + + public InProgressSequences with(MultiStepOperation.SequenceKey key, MultiStepOperation sequence) + { + if (contains(key)) + { + throw new Transformation.RejectedTransformationException(String.format("Can not add a new in-progress sequence for %s, " + + "since there's already one associated with it: %s", + key, + get(key))); + } + + ImmutableMap.Builder> builder = ImmutableMap.builder(); + builder.put(key, sequence); + for (Map.Entry> e : state.entrySet()) + { + if (e.getKey().equals(key)) + continue; + builder.put(e.getKey(), e.getValue()); + } + return new InProgressSequences(lastModified, builder.build()); + } + + public > InProgressSequences with(MultiStepOperation.SequenceKey key, Function update) + { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + for (Map.Entry> e : state.entrySet()) + { + if (e.getKey().equals(key)) + builder.put(e.getKey(), update.apply((T1) e.getValue())); + else + builder.put(e.getKey(), e.getValue()); + } + return new InProgressSequences(lastModified, builder.build()); + } + + public InProgressSequences without(MultiStepOperation.SequenceKey key) + { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + boolean removed = false; + for (Map.Entry> e : state.entrySet()) + { + if (e.getKey().equals(key)) + removed = true; + else + builder.put(e.getKey(), e.getValue()); + } + assert removed : String.format("Expected to remove an in-progress sequence for %s, but it wasn't found in in-progress sequences", key); + return new InProgressSequences(lastModified, builder.build()); + + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InProgressSequences that = (InProgressSequences) o; + return Objects.equals(state, that.state) && Objects.equals(lastModified, that.lastModified); + } + + @Override + public int hashCode() + { + return Objects.hash(state, lastModified); + } + + public static Set STARTUP_SEQUENCE_KINDS = ImmutableSet.of(MultiStepOperation.Kind.JOIN, MultiStepOperation.Kind.REPLACE); + + @VisibleForTesting + public static BiFunction, SequenceState, SequenceState> listener = (s, o) -> o; + + @VisibleForTesting + public static BiFunction, SequenceState, SequenceState> replaceListener(BiFunction, SequenceState, SequenceState> newListener) + { + BiFunction, SequenceState, SequenceState> prev = listener; + listener = newListener; + return prev; + } + + public static boolean resume(MultiStepOperation sequence) + { + SequenceState state; + if (sequence.barrier().await()) + state = listener.apply(sequence, sequence.executeNext()); + else + state = listener.apply(sequence, SequenceState.blocked()); + + if (state.isError()) + throw ((SequenceState.Error)state).cause(); + + return state.isContinuable(); + } + + public static boolean isLeave(MultiStepOperation sequence) + { + return sequence.kind() == LEAVE; + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(InProgressSequences t, DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(t.lastModified, out, version); + out.writeInt(t.state.size()); + for (Map.Entry> entry : t.state.entrySet()) + { + if (Version.UNKNOWN.isBefore(V2)) + { + NodeId.serializer.serialize((NodeId) entry.getKey(), out, version); + MultiStepOperation seq = entry.getValue(); + out.writeUTF(seq.kind().name()); + entry.getValue().kind().serializer.serialize(seq, out, version); + } + else + { + // Starting V2, we serialize the sequence first since we rely on the type during deserialization + MultiStepOperation seq = entry.getValue(); + out.writeUTF(seq.kind().name()); + entry.getValue().kind().serializer.serialize(seq, out, version); + MetadataSerializer keySerializer = (MetadataSerializer) seq.keySerializer(); + keySerializer.serialize(entry.getKey(), out, version); + } + } + } + + public InProgressSequences deserialize(DataInputPlus in, Version version) throws IOException + { + Epoch lastModified = Epoch.serializer.deserialize(in, version); + int ipsSize = in.readInt(); + ImmutableMap.Builder> res = ImmutableMap.builder(); + for (int i = 0; i < ipsSize; i++) + { + if (Version.UNKNOWN.isBefore(V2)) + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + MultiStepOperation.Kind kind = MultiStepOperation.Kind.valueOf(in.readUTF()); + MultiStepOperation ips = kind.serializer.deserialize(in, version); + res.put(nodeId, ips); + } + else + { + MultiStepOperation.Kind kind = MultiStepOperation.Kind.valueOf(in.readUTF()); + MultiStepOperation ips = kind.serializer.deserialize(in, version); + MultiStepOperation.SequenceKey key = ips.keySerializer().deserialize(in, version); + res.put(key, ips); + } + } + return new InProgressSequences(lastModified, res.build()); + } + + public long serializedSize(InProgressSequences t, Version version) + { + long size = Epoch.serializer.serializedSize(t.lastModified, version); + size += sizeof(t.state.size()); + for (Map.Entry> entry : t.state.entrySet()) + { + if (Version.UNKNOWN.isBefore(V2)) + { + size += NodeId.serializer.serializedSize((NodeId) entry.getKey(), version); + MultiStepOperation seq = entry.getValue(); + size += sizeof(seq.kind().name()); + size += entry.getValue().kind().serializer.serializedSize(seq, version); + } + else + { + MultiStepOperation seq = entry.getValue(); + size += sizeof(seq.kind().name()); + size += entry.getValue().kind().serializer.serializedSize(seq, version); + MetadataSerializer keySerializer = (MetadataSerializer) seq.keySerializer(); + size += keySerializer.serializedSize(entry.getKey(), version); + } + } + return size; + } + } + + @Override + public String toString() + { + return "InProgressSequences{" + + "lastModified=" + lastModified + + ", state=" + state + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/LeaveStreams.java b/src/java/org/apache/cassandra/tcm/sequences/LeaveStreams.java new file mode 100644 index 0000000000..16257e4046 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/LeaveStreams.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.tcm.sequences; + +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.transformations.Assassinate; + +public interface LeaveStreams +{ + void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave) throws ExecutionException, InterruptedException; + Kind kind(); + String status(); + + enum Kind + { + UNBOOTSTRAP(UnbootstrapStreams::new), + REMOVENODE(RemoveNodeStreams::new), + ASSASSINATE(() -> Assassinate.LEAVE_STREAMS); + + public final Supplier supplier; + + Kind(Supplier streamsSupplier) + { + this.supplier = streamsSupplier; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/LockedRanges.java b/src/java/org/apache/cassandra/tcm/sequences/LockedRanges.java new file mode 100644 index 0000000000..c4890e64ee --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/LockedRanges.java @@ -0,0 +1,451 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataValue; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.db.TypeSizes.sizeof; + +public class LockedRanges implements MetadataValue +{ + public static final Serializer serializer = new Serializer(); + public static final LockedRanges EMPTY = new LockedRanges(Epoch.EMPTY, ImmutableMap.builder().build()); + public static final Key NOT_LOCKED = new Key(Epoch.EMPTY); + public final ImmutableMap locked; + private final Epoch lastModified; + + private LockedRanges(Epoch lastModified, ImmutableMap locked) + { + this.lastModified = lastModified; + this.locked = locked; + } + + public LockedRanges lock(Key key, AffectedRanges ranges) + { + assert !key.equals(NOT_LOCKED) : "Can't lock ranges with noop key"; + + if (ranges == AffectedRanges.EMPTY) + return this; + + // TODO might we need the ability for the holder of a key to lock multiple sets over time? + return new LockedRanges(lastModified, + ImmutableMap.builderWithExpectedSize(locked.size()) + .putAll(locked) + .put(key, ranges) + .build()); + } + + public LockedRanges unlock(Key key) + { + if (key.equals(NOT_LOCKED)) + return this; + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(locked.size()); + locked.forEach((k, r) -> { + if (!k.equals(key)) builder.put(k, r); + }); + return new LockedRanges(lastModified, builder.build()); + } + + public Key intersects(AffectedRanges ranges) + { + for (Map.Entry e : locked.entrySet()) + { + if (ranges.intersects(e.getValue())) + return e.getKey(); + } + return NOT_LOCKED; + } + + @Override + public LockedRanges withLastModified(Epoch epoch) + { + return new LockedRanges(epoch, locked); + } + + @Override + public Epoch lastModified() + { + return lastModified; + } + + @Override + public String toString() + { + return "LockedRanges{" + + "lastModified=" + lastModified + + ", locked=" + locked + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof LockedRanges)) return false; + + LockedRanges that = (LockedRanges) o; + // check the last modified epoch and set of lock keys match first + if ( !Objects.equals(lastModified, that.lastModified) || !Objects.equals(locked.keySet(), that.locked.keySet())) + return false; + + // now for each lock key, compare the AffectedRanges + for (Map.Entry entry : locked.entrySet()) + { + // AffectedRanges is a Map> + // so first check the keysets are the same, then do a pairwise compare on the sets of ranges + LockedRanges.AffectedRanges otherAffected = that.locked.get(entry.getKey()); + Map>> thisRangesByReplication = entry.getValue().asMap(); + Map>> thatRangesByReplication = otherAffected.asMap(); + if (!thisRangesByReplication.keySet().equals(thatRangesByReplication.keySet())) + return false; + + for (ReplicationParams replication : thisRangesByReplication.keySet()) + if (!thisRangesByReplication.get(replication).equals(thatRangesByReplication.get(replication))) + return false; + }; + return true; + } + + @Override + public int hashCode() + { + return Objects.hash(lastModified, locked); + } + + public static Key keyFor(Epoch epoch) + { + return new Key(epoch); + } + + public interface AffectedRangesBuilder + { + AffectedRangesBuilder add(ReplicationParams params, Range range); + AffectedRanges build(); + } + + public interface AffectedRanges + { + AffectedRanges EMPTY = new AffectedRanges() + { + public boolean intersects(AffectedRanges other) + { + return false; + } + + public void foreach(BiConsumer>> fn) {} + + @Override + public String toString() + { + return "EMPTY"; + } + + public Map>> asMap() + { + return Collections.emptyMap(); + } + }; + + default ImmutableSet toPeers(ReplicationParams replication, DataPlacements placements, Directory directory) + { + ImmutableSet.Builder peers = ImmutableSet.builder(); + DataPlacement placement = placements.get(replication); + asMap().get(replication).stream() + .flatMap(range -> placement.affectedReplicas(range).stream()) + .map(directory::peerId) + .forEach(peers::add); + return peers.build(); + } + + static AffectedRanges singleton(ReplicationParams replicationParams, Range tokenRange) + { + return builder().add(replicationParams, tokenRange).build(); + } + + static AffectedRangesBuilder builder() + { + return new AffectedRangesImpl(); + } + + boolean intersects(AffectedRanges other); + void foreach(BiConsumer>> fn); + Map>> asMap(); + + final class Serializer implements MetadataSerializer + { + public static final Serializer instance = new Serializer(); + + public void serialize(AffectedRanges t, DataOutputPlus out, Version version) throws IOException + { + Map>> map = t.asMap(); + out.writeInt(map.size()); + for (Map.Entry>> rangeEntry : map.entrySet()) + { + ReplicationParams params = rangeEntry.getKey(); + Set> ranges = rangeEntry.getValue(); + ReplicationParams.serializer.serialize(params, out, version); + out.writeInt(ranges.size()); + for (Range range : ranges) + { + Token.metadataSerializer.serialize(range.left, out, version); + Token.metadataSerializer.serialize(range.right, out, version); + } + } + } + + public AffectedRanges deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + Map>> map = Maps.newHashMapWithExpectedSize(size); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int x = 0; x < size; x++) + { + ReplicationParams params = ReplicationParams.serializer.deserialize(in, version); + int rangeSize = in.readInt(); + Set> range = Sets.newHashSetWithExpectedSize(rangeSize); + for (int y = 0; y < rangeSize; y++) + { + range.add(new Range<>(Token.metadataSerializer.deserialize(in, partitioner, version), + Token.metadataSerializer.deserialize(in, partitioner, version))); + } + map.put(params, range); + } + return new AffectedRangesImpl(map); + } + + public long serializedSize(AffectedRanges t, Version version) + { + Map>> map = t.asMap(); + long size = sizeof(map.size()); + for (Map.Entry>> rangeEntry : map.entrySet()) + { + ReplicationParams params = rangeEntry.getKey(); + Set> ranges = rangeEntry.getValue(); + size += ReplicationParams.serializer.serializedSize(params, version); + size += sizeof(ranges.size()); + for (Range range : ranges) + { + size += Token.metadataSerializer.serializedSize(range.left, version); + size += Token.metadataSerializer.serializedSize(range.right, version); + } + } + return size; + } + } + } + + private static final class AffectedRangesImpl implements AffectedRangesBuilder, AffectedRanges + { + private final Map>> map; + + public AffectedRangesImpl() + { + this(new HashMap<>()); + } + + public AffectedRangesImpl(Map>> map) + { + this.map = map; + } + + @Override + public AffectedRangesBuilder add(ReplicationParams params, Range range) + { + Set> ranges = map.get(params); + if (ranges == null) + { + ranges = new HashSet<>(); + map.put(params, ranges); + } + + ranges.add(range); + return this; + } + + @Override + public Map>> asMap() + { + return map; + } + + @Override + public AffectedRanges build() + { + return this; + } + + @Override + public void foreach(BiConsumer>> fn) + { + map.forEach((k, v) -> fn.accept(k, Collections.unmodifiableSet(v))); + } + + @Override + public boolean intersects(AffectedRanges other) + { + if (other == EMPTY) + return false; + + for (Map.Entry>> e : ((AffectedRangesImpl) other).map.entrySet()) + { + for (Range otherRange : e.getValue()) + { + for (Range thisRange : map.get(e.getKey())) + { + if (thisRange.intersects(otherRange)) + return true; + } + } + } + + return false; + } + + @Override + public String toString() + { + return "AffectedRangesImpl{" + + "map=" + map + + '}'; + } + } + + public static class Key + { + public static final Serializer serializer = new Serializer(); + private final Epoch epoch; + + private Key(Epoch epoch) + { + this.epoch = epoch; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key1 = (Key) o; + return epoch.equals(key1.epoch); + } + + @Override + public int hashCode() + { + return Objects.hash(epoch); + } + + @Override + public String toString() + { + return "Key{" + + "key=" + epoch + + '}'; + } + + public static final class Serializer + { + public void serialize(Key t, DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(t.epoch, out, version); + } + + public Key deserialize(DataInputPlus in, Version version) throws IOException + { + return new Key(Epoch.serializer.deserialize(in, version)); + } + + public long serializedSize(Key t, Version version) + { + return Epoch.serializer.serializedSize(t.epoch, version); + } + } + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(LockedRanges t, DataOutputPlus out, Version version) throws IOException + { + Epoch.serializer.serialize(t.lastModified, out, version); + out.writeInt(t.locked.size()); + for (Map.Entry entry : t.locked.entrySet()) + { + Key key = entry.getKey(); + Epoch.serializer.serialize(key.epoch, out, version); + AffectedRanges.Serializer.instance.serialize(entry.getValue(), out, version); + } + } + + public LockedRanges deserialize(DataInputPlus in, Version version) throws IOException + { + Epoch lastModified = Epoch.serializer.deserialize(in, version); + int size = in.readInt(); + if (size == 0) return new LockedRanges(lastModified, ImmutableMap.of()); + ImmutableMap.Builder result = ImmutableMap.builder(); + for (int i = 0; i < size; i++) + { + Key key = new Key(Epoch.serializer.deserialize(in, version)); + AffectedRanges ranges = AffectedRanges.Serializer.instance.deserialize(in, version); + result.put(key, ranges); + } + return new LockedRanges(lastModified, result.build()); + } + + public long serializedSize(LockedRanges t, Version version) + { + long size = Epoch.serializer.serializedSize(t.lastModified, version); + size += sizeof(t.locked.size()); + for (Map.Entry entry : t.locked.entrySet()) + { + Key key = entry.getKey(); + size += Epoch.serializer.serializedSize(key.epoch, version); + size += AffectedRanges.Serializer.instance.serializedSize(entry.getValue(), version); + } + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/Move.java b/src/java/org/apache/cassandra/tcm/sequences/Move.java new file mode 100644 index 0000000000..1210969ea0 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/Move.java @@ -0,0 +1,586 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.gms.IFailureDetector; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.streaming.StreamPlan; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.PrepareMove; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_MOVE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_MOVE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_MOVE; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.MOVE; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; +import static org.apache.cassandra.tcm.sequences.SequenceState.error; + +public class Move extends MultiStepOperation +{ + private static final Logger logger = LoggerFactory.getLogger(Move.class); + public static final Serializer serializer = new Serializer(); + + public final Collection tokens; + public final LockedRanges.Key lockKey; + public final PlacementDeltas toSplitRanges; + public final PrepareMove.StartMove startMove; + public final PrepareMove.MidMove midMove; + public final PrepareMove.FinishMove finishMove; + public final boolean streamData; + public final Transformation.Kind next; + + public static Move newSequence(Epoch preparedAt, + LockedRanges.Key lockKey, + Collection tokens, + PlacementDeltas toSplitRanges, + PrepareMove.StartMove startMove, + PrepareMove.MidMove midMove, + PrepareMove.FinishMove finishMove, + boolean streamData) + { + return new Move(preparedAt, + lockKey, + START_MOVE, + tokens, + toSplitRanges, + startMove, midMove, finishMove, + streamData); + } + + /** + * Used by factory method for external callers and by Serializer + */ + @VisibleForTesting + Move(Epoch latestModification, + LockedRanges.Key lockKey, + Transformation.Kind next, + Collection tokens, + PlacementDeltas toSplitRanges, + PrepareMove.StartMove startMove, + PrepareMove.MidMove midMove, + PrepareMove.FinishMove finishMove, + boolean streamData) + { + super(nextToIndex(next), latestModification); + this.lockKey = lockKey; + this.next = next; + this.tokens = tokens; + this.toSplitRanges = toSplitRanges; + this.startMove = startMove; + this.midMove = midMove; + this.finishMove = finishMove; + this.streamData = streamData; + } + + /** + * Used by advance to move forward in the sequence after execution + */ + private Move(Move current, Epoch latestModification) + { + super(current.idx + 1, latestModification); + this.next = indexToNext(current.idx + 1); + this.lockKey = current.lockKey; + this.tokens = current.tokens; + this.toSplitRanges = current.toSplitRanges; + this.startMove = current.startMove; + this.midMove = current.midMove; + this.finishMove = current.finishMove; + this.streamData = current.streamData; + } + + @Override + public Kind kind() + { + return MOVE; + } + + @Override + protected SequenceKey sequenceKey() + { + return startMove.nodeId(); + } + + @Override + public MetadataSerializer keySerializer() + { + return NodeId.serializer; + } + + @Override public Transformation.Kind nextStep() + { + return indexToNext(idx); + } + + @Override + public SequenceState executeNext() + { + switch (next) + { + case START_MOVE: + try + { + ClusterMetadata metadata = ClusterMetadata.current(); + logger.info("Moving {} from {} to {}.", + metadata.directory.endpoint(startMove.nodeId()), + metadata.tokenMap.tokens(startMove.nodeId()), + finishMove.newTokens); + ClusterMetadataService.instance().commit(startMove); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return continuable() ; + } + break; + case MID_MOVE: + try + { + logger.info("fetching new ranges and streaming old ranges"); + StreamPlan streamPlan = new StreamPlan(StreamOperation.RELOCATION); + Keyspaces keyspaces = Schema.instance.getNonLocalStrategyKeyspaces(); + Map movementMap = movementMap(FailureDetector.instance, + ClusterMetadata.current().placements, + toSplitRanges, + startMove.delta(), + midMove.delta(), + StorageService.useStrictConsistency) + .asMap(); + + for (KeyspaceMetadata ks : keyspaces) + { + ReplicationParams replicationParams = ks.params.replication; + if (replicationParams.isMeta()) + continue; + EndpointsByReplica endpoints = movementMap.get(replicationParams); + for (Map.Entry e : endpoints.flattenEntries()) + { + Replica destination = e.getKey(); + Replica source = e.getValue(); + logger.info("Stream source: {} destination: {}", source, destination); + assert !source.endpoint().equals(destination.endpoint()) : String.format("Source %s should not be the same as destionation %s", source, destination); + if (source.isSelf()) + streamPlan.transferRanges(destination.endpoint(), ks.name, RangesAtEndpoint.of(destination)); + else if (destination.isSelf()) + { + if (destination.isFull()) + streamPlan.requestRanges(source.endpoint(), ks.name, RangesAtEndpoint.of(destination), RangesAtEndpoint.empty(destination.endpoint())); + else + streamPlan.requestRanges(source.endpoint(), ks.name, RangesAtEndpoint.empty(destination.endpoint()), RangesAtEndpoint.of(destination)); + } + else + throw new IllegalStateException("Node should be either source or destination in the movement map " + endpoints); + } + } + + streamPlan.execute().get(); + StorageService.instance.repairPaxosForTopologyChange("move"); + } + catch (InterruptedException e) + { + return continuable(); + } + catch (ExecutionException e) + { + throw new RuntimeException("Unable to move", e); + } + + try + { + ClusterMetadataService.instance().commit(midMove); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return continuable(); + } + break; + case FINISH_MOVE: + try + { + SystemKeyspace.updateLocalTokens(tokens); + ClusterMetadataService.instance().commit(finishMove); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return continuable(); + } + + break; + default: + return error(new IllegalStateException("Can't proceed with join from " + next)); + } + + return continuable(); + } + + @Override + public Move advance(Epoch waitForWatermark) + { + return new Move(this, waitForWatermark); + } + + @Override + public ProgressBarrier barrier() + { + if (next == START_MOVE) + return ProgressBarrier.immediate(); + ClusterMetadata metadata = ClusterMetadata.current(); + return new ProgressBarrier(latestModification, metadata.directory.location(startMove.nodeId()), metadata.lockedRanges.locked.get(lockKey)); + } + + @Override + public ClusterMetadata.Transformer cancel(ClusterMetadata metadata) + { + DataPlacements placements = metadata.placements; + + switch (next) + { + case FINISH_MOVE: + placements = midMove.inverseDelta().apply(metadata.nextEpoch(), placements); + case MID_MOVE: + placements = startMove.inverseDelta().apply(metadata.nextEpoch(), placements); + case START_MOVE: + placements = toSplitRanges.invert().apply(metadata.nextEpoch(), placements); + break; + default: + throw new IllegalStateException("Can't revert move from " + next); + } + + LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey); + return metadata.transformer() + .withNodeState(startMove.nodeId(), NodeState.JOINED) + .with(placements) + .with(newLockedRanges); + } + + /** + * Returns a mapping of destination -> source*, where the destination is the node that needs to stream from source + * + * there can be multiple sources for each destination + */ + private static MovementMap movementMap(IFailureDetector fd, DataPlacements placements, PlacementDeltas toSplitRanges, PlacementDeltas toStart, PlacementDeltas midDeltas, boolean strictConsistency) + { + MovementMap.Builder allMovements = MovementMap.builder(); + toStart.forEach((params, delta) -> { + RangesByEndpoint targets = delta.writes.additions; + PlacementForRange oldOwners = placements.get(params).reads; + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + Iterables.concat(targets.flattenValues(), + transientToFullReplicas(midDeltas.get(params)).flattenValues()).forEach(destination -> { + SourceHolder sources = new SourceHolder(fd, destination, toSplitRanges.get(params), strictConsistency); + AtomicBoolean needsRelaxedSources = new AtomicBoolean(); + // first, try to find strict sources for the ranges we need to stream - these are the ranges that + // instances are losing. + midDeltas.get(params).reads.removals.flattenValues().forEach(strictSource -> { + if (strictSource.range().equals(destination.range()) && !strictSource.endpoint().equals(destination.endpoint())) + if (!sources.addSource(strictSource)) + { + if (!strictConsistency) + throw new IllegalStateException("Couldn't find any matching sufficient replica out of: " + strictSource + " -> " + destination); + needsRelaxedSources.set(true); + } + }); + + // if we are not running with strict consistency, try to find other sources for streaming + if (needsRelaxedSources.get()) + { + for (Replica source : DatabaseDescriptor.getEndpointSnitch().sortedByProximity(FBUtilities.getBroadcastAddressAndPort(), + oldOwners.forRange(destination.range()).get())) + { + if (fd.isAlive(source.endpoint()) && !source.endpoint().equals(destination.endpoint())) + { + if ((sources.fullSource == null && source.isFull()) || + (sources.transientSource == null && source.isTransient())) + sources.addSource(source); + } + } + } + + if (sources.fullSource == null && destination.isFull()) + throw new IllegalStateException("Found no sources for "+destination); + sources.addToMovements(destination, movements); + }); + allMovements.put(params, movements.build()); + }); + + return allMovements.build(); + } + + private static class SourceHolder + { + private final IFailureDetector fd; + private final PlacementDeltas.PlacementDelta splitDelta; + private final boolean strict; + private Replica fullSource; + private Replica transientSource; + private final Replica destination; + + public SourceHolder(IFailureDetector fd, Replica destination, PlacementDeltas.PlacementDelta splitDelta, boolean strict) + { + this.fd = fd; + this.splitDelta = splitDelta; + this.strict = strict; + this.destination = destination; + } + + private boolean addSource(Replica source) + { + if (fd.isAlive(source.endpoint())) + { + if (source.isFull()) + { + assert fullSource == null; + fullSource = source; + } + else + { + assert transientSource == null; + if (!destination.isSelf() && !source.isSelf()) + { + // a transient replica is being removed, now, to be able to safely skip streaming from this + // replica we need to make sure it remains a replica for the range after the move has finished: + if (splitDelta.writes.additions.get(source.endpoint()).byRange().get(destination.range()) == null) + { + if (strict) + throw new IllegalStateException(String.format("Source %s for %s is not remaining as a replica after the move, can't do a consistent range movement, retry with that disabled", source, destination)); + else + return false; + } + return true; + } + else + { + transientSource = source; + } + } + return true; + } + else if (strict) + throw new IllegalStateException("Strict consistency requires the node losing the range to be UP but " + source + " is DOWN"); + return false; + } + + private void addToMovements(Replica destination, EndpointsByReplica.Builder movements) + { + if (fullSource != null) + movements.put(destination, fullSource); + if (transientSource != null) + movements.put(destination, transientSource); + } + } + + private static RangesByEndpoint transientToFullReplicas(PlacementDeltas.PlacementDelta midDelta) + { + RangesByEndpoint.Builder builder = new RangesByEndpoint.Builder(); + midDelta.reads.additions.flattenValues().forEach((newReplica) -> { + if (newReplica.isFull()) + { + RangesAtEndpoint removals = midDelta.reads.removals.get(newReplica.endpoint()); + if (removals != null) + { + Replica removed = removals.byRange().get(newReplica.range()); + if (removed != null && removed.isTransient()) + builder.put(newReplica.endpoint(), newReplica); + } + } + }); + return builder.build(); + } + private static int nextToIndex(Transformation.Kind next) + { + switch (next) + { + case START_MOVE: + return 0; + case MID_MOVE: + return 1; + case FINISH_MOVE: + return 2; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", next, MOVE)); + } + } + + private static Transformation.Kind indexToNext(int index) + { + switch (index) + { + case 0: + return START_MOVE; + case 1: + return MID_MOVE; + case 2: + return FINISH_MOVE; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", index, MOVE)); + } + } + + @Override + public String toString() + { + return "Move{" + + "latestModification=" + latestModification + + ", tokens=" + tokens + + ", lockKey=" + lockKey + + ", toSplitRanges=" + toSplitRanges + + ", startMove=" + startMove + + ", midMove=" + midMove + + ", finishMove=" + finishMove + + ", streamData=" + streamData + + ", next=" + next + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof Move)) return false; + Move move = (Move) o; + return streamData == move.streamData && + next == move.next && + Objects.equals(latestModification, move.latestModification) && + Objects.equals(tokens, move.tokens) && + Objects.equals(lockKey, move.lockKey) && + Objects.equals(toSplitRanges, move.toSplitRanges) && + Objects.equals(startMove, move.startMove) && + Objects.equals(midMove, move.midMove) && + Objects.equals(finishMove, move.finishMove); + } + + @Override + public int hashCode() + { + return Objects.hash(latestModification, tokens, lockKey, next, toSplitRanges, startMove, midMove, finishMove, streamData); + } + + public static class Serializer implements AsymmetricMetadataSerializer, Move> + { + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + Move plan = (Move) t; + out.writeBoolean(plan.streamData); + + Epoch.serializer.serialize(plan.latestModification, out, version); + LockedRanges.Key.serializer.serialize(plan.lockKey, out, version); + PlacementDeltas.serializer.serialize(plan.toSplitRanges, out, version); + VIntCoding.writeUnsignedVInt32(plan.next.ordinal(), out); + + PrepareMove.StartMove.serializer.serialize(plan.startMove, out, version); + PrepareMove.MidMove.serializer.serialize(plan.midMove, out, version); + PrepareMove.FinishMove.serializer.serialize(plan.finishMove, out, version); + + out.writeUnsignedVInt32(plan.tokens.size()); + for (Token token : plan.tokens) + Token.metadataSerializer.serialize(token, out, version); + } + + public Move deserialize(DataInputPlus in, Version version) throws IOException + { + boolean streamData = in.readBoolean(); + + Epoch barrier = Epoch.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + PlacementDeltas toSplitRanges = PlacementDeltas.serializer.deserialize(in, version); + Transformation.Kind next = Transformation.Kind.values()[VIntCoding.readUnsignedVInt32(in)]; + + PrepareMove.StartMove startMove = PrepareMove.StartMove.serializer.deserialize(in, version); + PrepareMove.MidMove midMove = PrepareMove.MidMove.serializer.deserialize(in, version); + PrepareMove.FinishMove finishMove = PrepareMove.FinishMove.serializer.deserialize(in, version); + + int numTokens = in.readUnsignedVInt32(); + Set tokens = new HashSet<>(); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < numTokens; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + return new Move(barrier, lockKey, next, tokens, + toSplitRanges, startMove, midMove, finishMove, streamData); + } + + public long serializedSize(MultiStepOperation t, Version version) + { + Move plan = (Move) t; + long size = TypeSizes.BOOL_SIZE; + + size += Epoch.serializer.serializedSize(plan.latestModification, version); + size += LockedRanges.Key.serializer.serializedSize(plan.lockKey, version); + size += PlacementDeltas.serializer.serializedSize(plan.toSplitRanges, version); + + size += VIntCoding.computeVIntSize(plan.kind().ordinal()); + + size += PrepareMove.StartMove.serializer.serializedSize(plan.startMove, version); + size += PrepareMove.MidMove.serializer.serializedSize(plan.midMove, version); + size += PrepareMove.FinishMove.serializer.serializedSize(plan.finishMove, version); + + size += TypeSizes.sizeofUnsignedVInt(plan.tokens.size()); + for (Token token : plan.tokens) + size += Token.metadataSerializer.serializedSize(token, version); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/ProgressBarrier.java b/src/java/org/apache/cassandra/tcm/sequences/ProgressBarrier.java new file mode 100644 index 0000000000..3bc5db2782 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/ProgressBarrier.java @@ -0,0 +1,575 @@ +/* + * 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.tcm.sequences; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.Timer; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallbackWithFailure; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Retry; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.concurrent.AsyncPromise; + +/** + * ProgressBarrier is responsible for ensuring that epoch visibility plays together with quorum consistency. + * + * When bootstrapping a node, streaming will not start until (by default) EACH_QUORUM of nodes has seen the epoch that + * adds the joining jode to the write replica set. + * + * Each subsequent step will be gated by waiting for (by default) EACH_QUORUM of nodes in or proposed to be in the replica set + * to see the previous epoch. + * + * If number of nodes in the cluster is smaller than the number of nodes specified in the replication factor, we will + * collect only n/2 + 1 nodes to avoid availability issues. + */ +public class ProgressBarrier +{ + private static final Logger logger = LoggerFactory.getLogger(ProgressBarrier.class); + private static final ConsistencyLevel MIN_CL = DatabaseDescriptor.getProgressBarrierMinConsistencyLevel(); + private static final ConsistencyLevel DEFAULT_CL = DatabaseDescriptor.getProgressBarrierDefaultConsistencyLevel(); + private static final long TIMEOUT_MILLIS = DatabaseDescriptor.getProgressBarrierTimeout(TimeUnit.MILLISECONDS); + private static final long BACKOFF_MILLIS = DatabaseDescriptor.getProgressBarrierBackoff(TimeUnit.MILLISECONDS); + + public final Epoch waitFor; + // Location of the affected node; used for LOCAL_QUORUM + public final Location location; + public final LockedRanges.AffectedRanges affectedRanges; + public final MessageDelivery messagingService; + public final Predicate filter; + + public ProgressBarrier(Epoch waitFor, Location location, LockedRanges.AffectedRanges affectedRanges) + { + this(waitFor, location, affectedRanges, MessagingService.instance(), (t) -> true); + } + + public ProgressBarrier(Epoch waitFor, Location location, LockedRanges.AffectedRanges affectedRanges, Predicate filter) + { + this(waitFor, location, affectedRanges, MessagingService.instance(), filter); + } + + private ProgressBarrier(Epoch waitFor, Location location, LockedRanges.AffectedRanges affectedRanges, MessageDelivery messagingService, Predicate filter) + { + this.waitFor = waitFor; + this.affectedRanges = affectedRanges; + this.location = location; + this.messagingService = messagingService; + this.filter = filter; + } + + public static ProgressBarrier immediate() + { + return new ProgressBarrier(Epoch.EMPTY, null, LockedRanges.AffectedRanges.EMPTY); + } + + @VisibleForTesting + public ProgressBarrier withMessagingService(MessageDelivery messagingService) + { + return new ProgressBarrier(waitFor, location, affectedRanges, messagingService, filter); + } + + public boolean await() + { + try (Timer.Context ctx = TCMMetrics.instance.progressBarrierLatency.time()) + { + if (waitFor.is(Epoch.EMPTY)) + return true; + + ConsistencyLevel currentCL = DEFAULT_CL; + while (!await(currentCL, ClusterMetadata.current())) + { + if (currentCL == MIN_CL) + return false; + + ConsistencyLevel prev = currentCL; + currentCL = relaxConsistency(prev); + logger.info(String.format("Could not collect epoch acknowledgements within %dms for %s. Falling back to %s.", TIMEOUT_MILLIS, prev, currentCL)); + } + return true; + } + } + + @VisibleForTesting + public boolean await(ConsistencyLevel cl, ClusterMetadata metadata) + { + if (waitFor.is(Epoch.EMPTY)) + return true; + + int maxWaitFor = 0; + Map>> affectedRangesMap = affectedRanges.asMap(); + List waiters = new ArrayList<>(affectedRangesMap.size()); + + Set superset = new HashSet<>(); + + for (Map.Entry>> e : affectedRangesMap.entrySet()) + { + ReplicationParams params = e.getKey(); + Set> ranges = e.getValue(); + for (Range range : ranges) + { + EndpointsForRange writes = metadata.placements.get(params).writes.matchRange(range).get().filter(r -> filter.test(r.endpoint())); + EndpointsForRange reads = metadata.placements.get(params).reads.matchRange(range).get().filter(r -> filter.test(r.endpoint())); + reads.stream().map(Replica::endpoint).forEach(superset::add); + writes.stream().map(Replica::endpoint).forEach(superset::add); + + WaitFor waitFor; + switch (cl) + { + case ALL: + waitFor = new WaitForAll(writes, reads); + break; + case EACH_QUORUM: + waitFor = new WaitForEachQuorum(writes, reads, metadata.directory); + break; + case LOCAL_QUORUM: + waitFor = new WaitForLocalQuorum(writes, reads, metadata.directory, location); + break; + case QUORUM: + waitFor = new WaitForQuorum(writes, reads); + break; + case ONE: + waitFor = new WaitForOne(writes, reads); + break; + case NODE_LOCAL: + waitFor = new WaitForNone(); + break; + default: + throw new IllegalArgumentException("Progress barrier only supports ALL, EACH_QUORUM, LOCAL_QUORUM, QUORUM, ONE and NODE_LOCAL, but not " + cl); + } + + maxWaitFor = Math.max(waitFor.waitFor(), maxWaitFor); + waiters.add(waitFor); + } + } + + Set collected = new HashSet<>(); + Set requests = new HashSet<>(); + for (InetAddressAndPort peer : superset) + requests.add(new WatermarkRequest(peer, messagingService, waitFor)); + + Retry.Deadline deadline = Retry.Deadline.after(TimeUnit.MILLISECONDS.toNanos(TIMEOUT_MILLIS), + new Retry.Backoff(DatabaseDescriptor.getCmsDefaultRetryMaxTries(), + (int) BACKOFF_MILLIS, + TCMMetrics.instance.fetchLogRetries)); + while (!deadline.reachedMax()) + { + for (WatermarkRequest request : requests) + request.retry(); + long nextTimeout = Clock.Global.nanoTime() + DatabaseDescriptor.getRpcTimeout(TimeUnit.NANOSECONDS); + Iterator iter = requests.iterator(); + while (iter.hasNext()) + { + WatermarkRequest request = iter.next(); + if (request.condition.awaitUninterruptibly(Math.max(0, nextTimeout - Clock.Global.nanoTime()), TimeUnit.NANOSECONDS) && + request.condition.isSuccess()) + { + collected.add(request.to); + iter.remove(); + } + } + + // No need to try processing until we collect enough nodes to pass all conditions + if (collected.size() < maxWaitFor) + { + deadline.maybeSleep(); + continue; + } + + boolean match = true; + for (WaitFor waiter : waiters) + { + if (!waiter.satisfiedBy(collected)) + { + match = false; + break; + } + } + if (match) + { + logger.info("Collected acknowledgements from {} of nodes for a progress barrier for epoch {} at {}", + collected, waitFor, cl); + return true; + } + } + + Set remaining = new HashSet<>(superset); + remaining.removeAll(collected); + logger.warn("Could not collect {} of nodes for a progress barrier for epoch {} to finish within {}ms. Nodes that have not responded: {}", + cl, waitFor, TimeUnit.NANOSECONDS.toMillis(Clock.Global.nanoTime() - deadline.deadlineNanos), remaining); + return false; + } + + public static ConsistencyLevel relaxConsistency(ConsistencyLevel cl) + { + logger.debug("Relaxing ProgressBarrier consistency level {}", cl); + TCMMetrics.instance.progressBarrierCLRelax.mark(); + switch (cl) + { + case ALL: + return ConsistencyLevel.EACH_QUORUM; + case EACH_QUORUM: + return ConsistencyLevel.QUORUM; + case QUORUM: + return ConsistencyLevel.LOCAL_QUORUM; + case LOCAL_QUORUM: + return ConsistencyLevel.ONE; + case ONE: + return ConsistencyLevel.NODE_LOCAL; + default: + throw new IllegalArgumentException(cl.toString()); + } + } + + public static class WaitForNone implements WaitFor + { + public boolean satisfiedBy(Set responded) + { + return true; + } + + public int waitFor() + { + return 0; + } + } + + public static class WaitForOne implements WaitFor + { + final Set nodes; + + public WaitForOne(EndpointsForRange writes, EndpointsForRange reads) + { + this.nodes = Sets.newHashSetWithExpectedSize(reads.size() + 1); + writes.forEach(r -> nodes.add(r.endpoint())); + reads.forEach(r -> nodes.add(r.endpoint())); + } + + public boolean satisfiedBy(Set responded) + { + for (InetAddressAndPort node : nodes) + { + if (responded.contains(node)) + return true; + } + + return false; + } + + public int waitFor() + { + return 1; + } + + public String toString() + { + return "WaitForOne{" + + "nodes=" + nodes + + '}'; + } + } + + public static class WaitForQuorum implements WaitFor + { + final Set nodes; + final int waitFor; + + public WaitForQuorum(EndpointsForRange writes, EndpointsForRange reads) + { + this.nodes = Sets.newHashSetWithExpectedSize(reads.size() + 1); + writes.forEach(r -> nodes.add(r.endpoint())); + reads.forEach(r -> nodes.add(r.endpoint())); + this.waitFor = nodes.size() / 2 + 1; + } + + public boolean satisfiedBy(Set responded) + { + int collected = 0; + for (InetAddressAndPort node : nodes) + { + if (responded.contains(node)) + collected++; + } + + return collected >= waitFor; + } + + public int waitFor() + { + return waitFor; + } + + public String toString() + { + return "WaitForQuorum{" + + "nodes=" + nodes + + ", waitFor=" + waitFor + + '}'; + } + } + + public static class WaitForLocalQuorum implements WaitFor + { + final Set nodesInOurDc; + final int waitFor; + + public WaitForLocalQuorum(EndpointsForRange writes, EndpointsForRange reads, Directory directory, Location local) + { + this.nodesInOurDc = Sets.newHashSetWithExpectedSize(reads.size() + 1); + writes.forEach(r -> addNode(r, directory, local)); + reads.forEach(r -> addNode(r, directory, local)); + this.waitFor = nodesInOurDc.size() / 2 + 1; + } + + private void addNode(Replica r, Directory directory, Location local) + { + InetAddressAndPort endpoint = r.endpoint(); + String dc = directory.location(directory.peerId(endpoint)).datacenter; + if (dc.equals(local.datacenter)) + this.nodesInOurDc.add(endpoint); + } + + public boolean satisfiedBy(Set responded) + { + int collected = 0; + for (InetAddressAndPort addr : responded) + { + if (nodesInOurDc.contains(addr)) + collected++; + } + + return collected >= waitFor; + } + + public int waitFor() + { + return waitFor; + } + + public String toString() + { + return "WaitForLocalQuorum{" + + "nodes=" + nodesInOurDc + + ", waitFor=" + waitFor + + '}'; + } + } + + /** + * Probably you do not want to use this in production, but this is still quite useful for testing purposes, + * when you also use a CL ALL in combination with this, and make sure writes propagate where they're + * supposed to propagate, alongside with streaming. + */ + public static class WaitForAll implements WaitFor + { + final Set nodes; + final int waitFor; + + public WaitForAll(EndpointsForRange writes, EndpointsForRange reads) + { + this.nodes = Sets.newHashSetWithExpectedSize(reads.size() + 1); + writes.forEach(r -> nodes.add(r.endpoint())); + reads.forEach(r -> nodes.add(r.endpoint())); + this.waitFor = nodes.size(); + } + + public boolean satisfiedBy(Set responded) + { + int collected = 0; + for (InetAddressAndPort node : nodes) + { + if (responded.contains(node)) + collected++; + } + + return collected >= waitFor; + } + + public int waitFor() + { + return waitFor; + } + + public String toString() + { + return "WaitForLocalQuorum{" + + "nodes=" + nodes + + ", waitFor=" + waitFor + + '}'; + } + } + + public static class WaitForEachQuorum implements WaitFor + { + final Map> nodesByDc; + final Map waitForByDc; + final int waitForTotal; + + public WaitForEachQuorum(EndpointsForRange writes, EndpointsForRange reads, Directory directory) + { + nodesByDc = Maps.newHashMapWithExpectedSize(directory.knownDatacenters().size()); + writes.forEach((r) -> addToDc(r, directory)); + reads.forEach((r) -> addToDc(r, directory)); + waitForByDc = Maps.newHashMapWithExpectedSize(nodesByDc.size()); + int total = 0; + for (Map.Entry> e : nodesByDc.entrySet()) + { + int waitFor = e.getValue().size() / 2 + 1; + waitForByDc.put(e.getKey(), waitFor); + total += waitFor; + } + this.waitForTotal = total; + } + + private void addToDc(Replica r, Directory directory) + { + InetAddressAndPort endpoint = r.endpoint(); + String dc = directory.location(directory.peerId(endpoint)).datacenter; + nodesByDc.computeIfAbsent(dc, (dc_) -> Sets.newHashSetWithExpectedSize(3)) + .add(endpoint); + } + + public boolean satisfiedBy(Set responded) + { + for (Map.Entry> e : nodesByDc.entrySet()) + { + int waitFor = waitForByDc.get(e.getKey()); + int collected = 0; + for (InetAddressAndPort node : e.getValue()) + { + if (responded.contains(node)) + collected++; + } + if (collected < waitFor) + return false; + } + return true; + } + + public int waitFor() + { + return waitForTotal; + } + + public String toString() + { + return "WaitForEachQuorum{" + + "nodesByDc=" + nodesByDc + + ", waitForByDc=" + waitForByDc + + ", waitForTotal=" + waitForTotal + + '}'; + } + } + + public interface WaitFor + { + boolean satisfiedBy(Set responded); + int waitFor(); + } + + private static class WatermarkRequest implements RequestCallbackWithFailure + { + private AsyncPromise condition = null; + private final InetAddressAndPort to; + private final MessageDelivery messagingService; + private final Epoch waitFor; + + public WatermarkRequest(InetAddressAndPort to, MessageDelivery messagingService, Epoch waitFor) + { + + this.to = to; + this.messagingService = messagingService; + this.waitFor = waitFor; + } + + @Override + public void onResponse(Message msg) + { + Epoch remote = msg.payload; + if (remote.isEqualOrAfter(waitFor)) + { + logger.debug("Received watermark response from {} with epoch {}", msg.from(), remote); + condition.trySuccess(null); + } + else + { + condition.tryFailure(new TimeoutException(String.format("Watermark request returned epoch %s while least %s was expected.", remote, waitFor))); + } + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason) + { + logger.debug("Error response from {} with {}", from, failureReason); + condition.tryFailure(new TimeoutException(String.format("Watermark request did returned %s.", failureReason))); + } + + public void retry() + { + condition = new AsyncPromise<>(); + messagingService.sendWithCallback(Message.out(Verb.TCM_CURRENT_EPOCH_REQ, ClusterMetadata.current().epoch), to, this); + } + } + + @Override + public String toString() + { + return "ProgressBarrier{" + + "epoch=" + waitFor + + ", affectedPeers=" + affectedRanges + + '}'; + } + + @VisibleForTesting + public static void propagateLast(LockedRanges.AffectedRanges ranges) + { + ClusterMetadata metadata = ClusterMetadata.current(); + new ProgressBarrier(metadata.epoch, metadata.directory.location(metadata.myNodeId()), ranges).await(); + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java b/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java new file mode 100644 index 0000000000..a3e65eead6 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java @@ -0,0 +1,367 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.streaming.DataMovement; +import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.streaming.StreamPlan; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Retry; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.cms.AdvanceCMSReconfiguration; +import org.apache.cassandra.tcm.transformations.cms.PrepareCMSReconfiguration; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.Future; + +import static org.apache.cassandra.streaming.StreamOperation.RESTORE_REPLICA_COUNT; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +public class ReconfigureCMS extends MultiStepOperation +{ + public static final Serializer serializer = new Serializer(); + private static final Logger logger = LoggerFactory.getLogger(ReconfigureCMS.class); + + /** + * We store the state (lock key, diff, active transition, position in the logical sequence, latest epoch enacted + * as part of this sequence) in the singleton transformation itself. This simplifies access to that state by the + * transformation and makes its representation in `Enacted` log messages and the virtual log table more useful. + */ + public final AdvanceCMSReconfiguration next; + + /** + * Factory method, called when intiating a sequence to reconfigure the membership of the CMS. Supplies Epoch.EMPTY + * as the progress barrier condition as an entirely new reconfiguration sequence has no prerequisite. + * @param lockKey token which prevents intersecting operations being run concurrently. Due to the scope and nature + * of this particular operation this key always covers the entire cluster, effectively preventing + * multiple CMS reconfigurations from being prepared concurrently. It is stored on the sequence + * itself so that it can be released when the sequence completes or is cancelled. + * @param diff The set of add member / remove member operations that must be executed to transform the CMS + * membership between the initial and desired states. + */ + public static ReconfigureCMS newSequence(LockedRanges.Key lockKey, PrepareCMSReconfiguration.Diff diff) + { + return new ReconfigureCMS(new AdvanceCMSReconfiguration(0, Epoch.EMPTY, lockKey, diff, null)); + } + + /** + * Called by the factory method and deserializer. + * The supplied transformation represents the next step in the logical sequence. + * @param next step to be executed next + */ + private ReconfigureCMS(AdvanceCMSReconfiguration next) + { + super(next.sequenceIndex, next.latestModification); + this.next = next; + } + @Override + public Kind kind() + { + return MultiStepOperation.Kind.RECONFIGURE_CMS; + } + @Override + protected SequenceKey sequenceKey() + { + return SequenceKey.instance; + } + + @Override + public MetadataSerializer keySerializer() + { + return SequenceKey.serializer; + } + + @Override public Transformation.Kind nextStep() + { + return next.kind(); + } + + @Override + public SequenceState executeNext() + { + ClusterMetadata metadata = ClusterMetadata.current(); + MultiStepOperation sequence = metadata.inProgressSequences.get(SequenceKey.instance); + if (sequence.kind() != MultiStepOperation.Kind.RECONFIGURE_CMS) + throw new IllegalStateException(String.format("Can not advance in-progress sequence, since its kind is %s, but not %s", sequence.kind(), MultiStepOperation.Kind.RECONFIGURE_CMS)); + + ReconfigureCMS transitionCMS = (ReconfigureCMS) sequence; + try + { + if (transitionCMS.next.activeTransition != null) + { + // An active transition represents a joining member which has been added as a write replica, but must + // stream up to date distributed log tables before being able to serve reads & participate in quorums. + // If this is the case, do that streaming now. + ActiveTransition activeTransition = transitionCMS.next.activeTransition; + InetAddressAndPort endpoint = metadata.directory.endpoint(activeTransition.nodeId); + Replica replica = new Replica(endpoint, entireRange, true); + streamRanges(replica, activeTransition.streamCandidates); + } + // Commit the next step in the sequence + ClusterMetadataService.instance().commit(transitionCMS.next); + return SequenceState.continuable(); + } + catch (Throwable t) + { + logger.error("Could not finish adding the node to the Cluster Metadata Service", t); + return SequenceState.blocked(); + } + } + + @Override + public ReconfigureCMS advance(AdvanceCMSReconfiguration next) + { + return new ReconfigureCMS(next); + } + + @Override + public ProgressBarrier barrier() + { + ClusterMetadata metadata = ClusterMetadata.current(); + return new ProgressBarrier(latestModification, + metadata.directory.location(metadata.myNodeId()), + EntireRange.affectedRanges(metadata)); + } + + public static void maybeReconfigureCMS(ClusterMetadata metadata, InetAddressAndPort toRemove) + { + if (!metadata.fullCMSMembers().contains(toRemove)) + return; + + // We can force removal from the CMS as it doesn't alter the size of the service + ClusterMetadataService.instance().commit(new PrepareCMSReconfiguration.Simple(metadata.directory.peerId(toRemove))); + + InProgressSequences.finishInProgressSequences(SequenceKey.instance); + if (ClusterMetadata.current().isCMSMember(toRemove)) + throw new IllegalStateException(String.format("Could not remove %s from CMS", toRemove)); + } + + private static void initiateRemoteStreaming(Replica replicaForStreaming, Set streamCandidates) + { + ClusterMetadata metadata = ClusterMetadata.current(); + EndpointsForRange.Builder efr = EndpointsForRange.builder(entireRange); + streamCandidates.forEach(addr -> efr.add(new Replica(addr, entireRange, true))); + + MovementMap movements = MovementMap.builder().put(ReplicationParams.meta(metadata), + new EndpointsByReplica(Collections.singletonMap(replicaForStreaming, efr.build()))) + .build(); + + String operationId = replicaForStreaming.toString(); + DataMovements.ResponseTracker responseTracker = DataMovements.instance.registerMovements(RESTORE_REPLICA_COUNT, operationId, movements); + movements.byEndpoint().forEach((ep, epMovements) -> { + DataMovement msg = new DataMovement(operationId, RESTORE_REPLICA_COUNT.name(), epMovements); + MessagingService.instance().sendWithCallback(Message.out(Verb.INITIATE_DATA_MOVEMENTS_REQ, msg), ep, response -> { + logger.debug("Endpoint {} starting streams {}", response.from(), epMovements); + }); + }); + + try + { + responseTracker.await(); + } + finally + { + DataMovements.instance.unregisterMovements(RESTORE_REPLICA_COUNT, operationId); + } + } + + public static void streamRanges(Replica replicaForStreaming, Set streamCandidates) throws ExecutionException, InterruptedException + { + InetAddressAndPort endpoint = replicaForStreaming.endpoint(); + + // Current node is the streaming target. We can pick any other live CMS node as a streaming source + if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort())) + { + StreamPlan streamPlan = new StreamPlan(StreamOperation.BOOTSTRAP, 1, true, null, PreviewKind.NONE); + Optional streamingSource = streamCandidates.stream().filter(FailureDetector.instance::isAlive).findFirst(); + if (!streamingSource.isPresent()) + throw new IllegalStateException(String.format("Can not start range streaming as all candidates (%s) are down", streamCandidates)); + streamPlan.requestRanges(streamingSource.get(), + SchemaConstants.METADATA_KEYSPACE_NAME, + new RangesAtEndpoint.Builder(FBUtilities.getBroadcastAddressAndPort()).add(replicaForStreaming).build(), + new RangesAtEndpoint.Builder(FBUtilities.getBroadcastAddressAndPort()).build(), + DistributedMetadataLogKeyspace.TABLE_NAME); + streamPlan.execute().get(); + } + // Current node is a live CMS node, therefore the streaming source + else if (streamCandidates.contains(FBUtilities.getBroadcastAddressAndPort())) + { + StreamPlan streamPlan = new StreamPlan(StreamOperation.BOOTSTRAP, 1, true, null, PreviewKind.NONE); + streamPlan.transferRanges(endpoint, + SchemaConstants.METADATA_KEYSPACE_NAME, + new RangesAtEndpoint.Builder(replicaForStreaming.endpoint()).add(replicaForStreaming).build(), + DistributedMetadataLogKeyspace.TABLE_NAME); + streamPlan.execute().get(); + } + // We are neither a target, nor a source, so initiate streaming on the target + else + { + initiateRemoteStreaming(replicaForStreaming, streamCandidates); + } + + } + + static void repairPaxosTopology() + { + Retry.Backoff retry = new Retry.Backoff(TCMMetrics.instance.repairPaxosTopologyRetries); + List>> remaining = ActiveRepairService.instance().repairPaxosForTopologyChangeAsync(SchemaConstants.METADATA_KEYSPACE_NAME, + Collections.singletonList(entireRange), + "bootstrap"); + + while (!retry.reachedMax()) + { + Map>, Future> tasks = new HashMap<>(); + for (Supplier> supplier : remaining) + tasks.put(supplier, supplier.get()); + remaining.clear(); + logger.info("Performing paxos topology repair on: {}", remaining); + + for (Map.Entry>, Future> e : tasks.entrySet()) + { + try + { + e.getValue().get(); + } + catch (ExecutionException t) + { + logger.error("Caught an exception while repairing paxos topology.", t); + remaining.add(e.getKey()); + } + catch (InterruptedException t) + { + return; + } + } + + if (remaining.isEmpty()) + return; + + retry.maybeSleep(); + } + logger.error("Added node as a CMS, but failed to repair paxos topology after this operation."); + } + + public static class ActiveTransition + { + public final NodeId nodeId; + public final Set streamCandidates; + + public ActiveTransition(NodeId nodeId, Set streamCandidates) + { + this.nodeId = nodeId; + this.streamCandidates = Collections.unmodifiableSet(streamCandidates); + } + + @Override + public String toString() + { + return "ActiveTransition{" + + "nodeId=" + nodeId + + ", streamCandidates=" + streamCandidates + + '}'; + } + } + + public static class Serializer implements AsymmetricMetadataSerializer, ReconfigureCMS> + { + + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + ReconfigureCMS transformation = (ReconfigureCMS) t; + AdvanceCMSReconfiguration.serializer.serialize(transformation.next, out, version); + } + + public ReconfigureCMS deserialize(DataInputPlus in, Version version) throws IOException + { + return new ReconfigureCMS(AdvanceCMSReconfiguration.serializer.deserialize(in, version)); + } + + public long serializedSize(MultiStepOperation t, Version version) + { + ReconfigureCMS transformation = (ReconfigureCMS) t; + return AdvanceCMSReconfiguration.serializer.serializedSize(transformation.next, version); + } + } + + public static class SequenceKey implements MultiStepOperation.SequenceKey + { + public static SequenceKey instance = new SequenceKey(); + public static Serializer serializer = new Serializer(); + + private SequenceKey(){} + + public static class Serializer implements MetadataSerializer + { + + public void serialize(SequenceKey t, DataOutputPlus out, Version version) throws IOException + { + // not actually serialized at only one reconfiguration sequence + // is permitted at a time so the key is a constant + } + + public SequenceKey deserialize(DataInputPlus in, Version version) throws IOException + { + return instance; + } + + public long serializedSize(SequenceKey t, Version version) + { + return 0; + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/RemoveNodeStreams.java b/src/java/org/apache/cassandra/tcm/sequences/RemoveNodeStreams.java new file mode 100644 index 0000000000..d286a0f3e7 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/RemoveNodeStreams.java @@ -0,0 +1,165 @@ +/* + * 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.tcm.sequences; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.locator.SystemStrategy; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.streaming.DataMovement; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementForRange; + +import static org.apache.cassandra.streaming.StreamOperation.RESTORE_REPLICA_COUNT; + +public class RemoveNodeStreams implements LeaveStreams +{ + private static final Logger logger = LoggerFactory.getLogger(UnbootstrapStreams.class); + private final AtomicBoolean finished = new AtomicBoolean(); + private final AtomicBoolean failed = new AtomicBoolean(); + private DataMovements.ResponseTracker responseTracker; + + @Override + public void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave) throws ExecutionException, InterruptedException + { + ClusterMetadata metadata = ClusterMetadata.current(); + MovementMap movements = movementMap(metadata.directory.endpoint(leaving), + metadata, + startLeave, + midLeave, + finishLeave); + movements.forEach((params, eps) -> logger.info("Removenode movements: {}: {}", params, eps)); + String operationId = leaving.toUUID().toString(); + responseTracker = DataMovements.instance.registerMovements(RESTORE_REPLICA_COUNT, operationId, movements); + movements.byEndpoint().forEach((endpoint, epMovements) -> { + DataMovement msg = new DataMovement(operationId, RESTORE_REPLICA_COUNT.name(), epMovements); + MessagingService.instance().sendWithCallback(Message.out(Verb.INITIATE_DATA_MOVEMENTS_REQ, msg), endpoint, response -> { + logger.debug("Endpoint {} starting streams {}", response.from(), epMovements); + }); + }); + + try + { + responseTracker.await(); + finished.set(true); + } + catch (Exception e) + { + failed.set(true); + throw e; + } + finally + { + DataMovements.instance.unregisterMovements(RESTORE_REPLICA_COUNT, operationId); + } + } + + @Override + public Kind kind() + { + return Kind.REMOVENODE; + } + + public String status() + { + if (finished.get()) + return "streaming finished"; + if (failed.get()) + return "streaming failed"; + if (responseTracker == null) + return "streaming not yet started"; + return responseTracker.remaining() + .stream() + .map(i -> i.toString(true)) + .collect(Collectors.joining(",", "Waiting on streaming responses from: ", "")); + } + + /** + * create a map where the key is the destination, and the values are possible sources + * @return + */ + private static MovementMap movementMap(InetAddressAndPort leaving, ClusterMetadata metadata, PlacementDeltas startDelta, PlacementDeltas midDelta, PlacementDeltas finishDelta) + { + MovementMap.Builder allMovements = MovementMap.builder(); + // map of dest->src* movements, keyed by replication settings. During unbootstrap, this will be used to construct + // a stream plan for each keyspace, based on their replication params. + startDelta.forEach((params, delta) -> { + // no streaming for LocalStrategy and friends + if (SystemStrategy.class.isAssignableFrom(params.klass)) + return; + + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + RangesByEndpoint startWriteAdditions = startDelta.get(params).writes.additions; + // find current placements from the metadata, we need to stream from replicas that are not changed and are therefore not in the deltas + PlacementForRange currentPlacements = metadata.placements.get(params).reads; + startWriteAdditions.flattenValues() + .forEach(newReplica -> { + EndpointsForRange.Builder candidateBuilder = new EndpointsForRange.Builder(newReplica.range()); + currentPlacements.forRange(newReplica.range()).get().forEach(replica -> { + if (!replica.endpoint().equals(leaving) && !replica.endpoint().equals(newReplica.endpoint())) + candidateBuilder.add(replica, ReplicaCollection.Builder.Conflict.NONE); + }); + movements.putAll(newReplica, candidateBuilder.build(), ReplicaCollection.Builder.Conflict.NONE); + }); + // and check if any replicas went from transient -> full: + for (Replica removal : finishDelta.get(params).writes.removals.flattenValues()) + { + if (removal.isTransient()) + { + // if a replica (ignoring transientness) is being added as a read replica in midJoin, but removed as + // a write replica in finishJoin (the "removal" replica) it means it must have changed from transient + // to full (or the other way round) + RangesByEndpoint midReadAdditions = midDelta.get(params).reads.additions; + Replica toStream = midReadAdditions.get(removal.endpoint()).byRange().get(removal.range()); + if (toStream != null && toStream.isFull()) + { + logger.debug("Conversion from transient to full replica {} -> {}", removal, toStream); + EndpointsForRange.Builder candidateBuilder = new EndpointsForRange.Builder(removal.range()); + currentPlacements.forRange(removal.range()).get().forEach(replica -> { + if (!replica.endpoint().equals(leaving) && !replica.endpoint().equals(removal.endpoint())) + candidateBuilder.add(replica, ReplicaCollection.Builder.Conflict.NONE); + }); + EndpointsForRange sources = candidateBuilder.build(); + logger.debug("Streaming transient -> full conversion to {} from {}", removal, sources); + // `removal` is losing this transient range, but gaining the same full range, meaning we need to stream it in. + movements.putAll(removal, sources, ReplicaCollection.Builder.Conflict.NONE); + } + } + } + allMovements.put(params, movements.build()); + }); + return allMovements.build(); + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/ReplaceSameAddress.java b/src/java/org/apache/cassandra/tcm/sequences/ReplaceSameAddress.java new file mode 100644 index 0000000000..2c4553a14f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/ReplaceSameAddress.java @@ -0,0 +1,99 @@ +/* + * 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.tcm.sequences; + +import java.util.stream.StreamSupport; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.MovementMap; + +public class ReplaceSameAddress +{ + private static final Logger logger = LoggerFactory.getLogger(ReplaceSameAddress.class); + + public static MovementMap movementMap(NodeId nodeId, ClusterMetadata metadata) + { + MovementMap.Builder builder = MovementMap.builder(); + InetAddressAndPort addr = metadata.directory.endpoint(nodeId); + metadata.placements.forEach((params, placement) -> { + EndpointsByReplica.Builder sources = new EndpointsByReplica.Builder(); + placement.reads.byEndpoint().get(addr).forEach(destination -> { + placement.reads.forRange(destination.range()).forEach(potentialSource -> { + if (!potentialSource.endpoint().equals(addr)) + sources.put(destination, potentialSource); + }); + }); + builder.put(params, sources.build()); + }); + return builder.build(); + } + + public static void streamData(NodeId nodeId, ClusterMetadata metadata, boolean shouldBootstrap, boolean finishJoiningRing) + { + BootstrapAndReplace.checkUnsafeReplace(shouldBootstrap); + + //only go into hibernate state if replacing the same address (CASSANDRA-8523) + logger.warn("Writes will not be forwarded to this node during replacement because it has the same address as " + + "the node to be replaced ({}). If the previous node has been down for longer than max_hint_window, " + + "repair must be run after the replacement process in order to make this node consistent.", + DatabaseDescriptor.getReplaceAddress()); + + BootstrapAndReplace.gossipStateToHibernate(metadata, nodeId); + + SystemKeyspace.updateLocalTokens(metadata.tokenMap.tokens(nodeId)); + + if (shouldBootstrap) + { + boolean dataAvailable = BootstrapAndJoin.bootstrap(metadata.tokenMap.tokens(nodeId), + StorageService.INDEFINITE, + metadata, + metadata.directory.endpoint(nodeId), + movementMap(nodeId, metadata), + null); + + if (!dataAvailable) + { + logger.warn("Some data streaming failed. Use nodetool to check bootstrap state and resume. " + + "For more, see `nodetool help bootstrap`. {}", SystemKeyspace.getBootstrapState()); + throw new IllegalStateException("Could not finish join for during replacement"); + } + } + + if (finishJoiningRing) + { + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + StreamSupport.stream(ColumnFamilyStore.all().spliterator(), false) + .filter(cfs -> Schema.instance.getUserKeyspaces().names().contains(cfs.keyspace.getName())) + .forEach(cfs -> cfs.indexManager.executePreJoinTasksBlocking(true)); + Gossiper.instance.mergeNodeToGossip(metadata.myNodeId(), metadata); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/SequenceState.java b/src/java/org/apache/cassandra/tcm/sequences/SequenceState.java new file mode 100644 index 0000000000..ed054be4ea --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/SequenceState.java @@ -0,0 +1,124 @@ +/* + * 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.tcm.sequences; + +import java.io.Serializable; + +import com.google.common.annotations.VisibleForTesting; + +/** + * + */ +@VisibleForTesting +public abstract class SequenceState implements Serializable +{ + private static final SequenceState HALTED = new SequenceState("Halted due to non-fatal but incorrect state"){}; + private static final SequenceState BLOCKED = new SequenceState("Blocked on consensus from required participants"){}; + private static final SequenceState CONTINUING = new SequenceState("Continuable") + { + @Override + public boolean isContinuable() + { + return true; + } + }; + + public static SequenceState continuable() + { + return CONTINUING; + } + + public static SequenceState halted() + { + return HALTED; + } + + public static SequenceState blocked() + { + return BLOCKED; + } + + public static SequenceState error(Exception cause) + { + return new Error(cause); + } + + public static class Error extends SequenceState + { + private final RuntimeException cause; + private Error(Throwable cause) + { + super("Failed due to fatal error"); + this.cause = (cause instanceof RuntimeException) + ? (RuntimeException) cause + : new RuntimeException(cause); + } + + public RuntimeException cause() + { + return cause; + } + + public boolean isError() + { + return true; + } + } + + public final String label; + SequenceState(String label) + { + this.label = label; + } + + public boolean isContinuable() + { + return false; + } + + public boolean isError() + { + return false; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof SequenceState)) return false; + + // note: for Error instances, we don't compare the wrapped exceptions. + // this is a bit of a hack, but SequenceState acts like an enum except + // the Error instances are not constants as the exceptions they carry + // are attached dynamically. + return this.label.equals(((SequenceState) o).label); + } + + @Override + public int hashCode() + { + return label != null ? label.hashCode() : 0; + } + + @Override + public String toString() + { + return label; + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java new file mode 100644 index 0000000000..69c071120f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java @@ -0,0 +1,168 @@ +/* + * 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.tcm.sequences; + +import java.util.Collections; +import java.util.EnumSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareMove; + +import static org.apache.cassandra.service.StorageService.Mode.LEAVING; +import static org.apache.cassandra.service.StorageService.Mode.NORMAL; +import static org.apache.cassandra.service.StorageService.Mode.DECOMMISSION_FAILED; +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; + +/** + * This exists simply to group the static entrypoints to sequences that modify a single node + * e.g. decommission, remove, move + */ +public interface SingleNodeSequences +{ + Logger logger = LoggerFactory.getLogger(SingleNodeSequences.class); + + /** + * Entrypoint to begin node decommission process. + * + * @param shutdownNetworking if set to true, will also shut down networking on completion + * @param force if set to true, will decommission the node even if this would mean there will be not enough nodes + * to satisfy replication factor + */ + static void decommission(boolean shutdownNetworking, boolean force) + { + if (ClusterMetadataService.instance().isMigrating() || ClusterMetadataService.state() == ClusterMetadataService.State.GOSSIP) + throw new IllegalStateException("This cluster is migrating to cluster metadata, can't decommission until that is done."); + + ClusterMetadata metadata = ClusterMetadata.current(); + + StorageService.Mode mode = StorageService.instance.operationMode(); + if (!EnumSet.of(LEAVING, NORMAL, DECOMMISSION_FAILED).contains(mode)) + throw new UnsupportedOperationException("Node in " + mode + " state; wait for status to become normal"); + logger.debug("DECOMMISSIONING"); + + NodeId self = metadata.myNodeId(); + + ReconfigureCMS.maybeReconfigureCMS(metadata, getBroadcastAddressAndPort()); + MultiStepOperation inProgress = metadata.inProgressSequences.get(self); + + if (inProgress == null) + { + logger.info("starting decom with {} {}", metadata.epoch, self); + ClusterMetadataService.instance().commit(new PrepareLeave(self, + force, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + } + else if (!InProgressSequences.isLeave(inProgress)) + { + throw new IllegalArgumentException("Can not decommission a node that has an in-progress sequence"); + } + + InProgressSequences.finishInProgressSequences(self); + if (shutdownNetworking) + StorageService.instance.shutdownNetworking(); + } + + /** + * Entrypoint to begin node removal process + * + * @param toRemove id of the node to remove + * @param force if set to true, will remove the node even if this would mean there will be not enough nodes + * to satisfy replication factor + */ + static void removeNode(NodeId toRemove, boolean force) + { + ClusterMetadata metadata = ClusterMetadata.current(); + if (toRemove.equals(metadata.myNodeId())) + throw new UnsupportedOperationException("Cannot remove self"); + InetAddressAndPort endpoint = metadata.directory.endpoint(toRemove); + if (endpoint == null) + throw new UnsupportedOperationException("Host ID not found."); + if (Gossiper.instance.getLiveMembers().contains(endpoint)) + throw new UnsupportedOperationException("Node " + endpoint + " is alive and owns this ID. Use decommission command to remove it from the ring"); + + NodeState removeState = metadata.directory.peerState(toRemove); + if (removeState == null) + throw new UnsupportedOperationException("Node to be removed is not a member of the token ring"); + if (removeState == NodeState.LEAVING) + logger.warn("Node {} is already leaving or being removed, continuing removal anyway", endpoint); + + if (metadata.inProgressSequences.contains(toRemove)) + throw new UnsupportedOperationException("Can not remove a node that has an in-progress sequence"); + + ReconfigureCMS.maybeReconfigureCMS(metadata, endpoint); + + logger.info("starting removenode with {} {}", metadata.epoch, toRemove); + + ClusterMetadataService.instance().commit(new PrepareLeave(toRemove, + force, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.REMOVENODE)); + InProgressSequences.finishInProgressSequences(toRemove); + } + + /** + * move the node to new token or find a new token to boot to according to load + * + * @param newToken new token to boot to, or if null, find balanced token to boot to + */ + static void move(Token newToken) + { + if (ClusterMetadataService.instance().isMigrating() || ClusterMetadataService.state() == ClusterMetadataService.State.GOSSIP) + throw new IllegalStateException("This cluster is migrating to cluster metadata, can't move until that is done."); + + if (newToken == null) + throw new IllegalArgumentException("Can't move to the undefined (null) token."); + + if (ClusterMetadata.current().tokenMap.tokens().contains(newToken)) + throw new IllegalArgumentException(String.format("target token %s is already owned by another node.", newToken)); + + // address of the current node + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId self = metadata.myNodeId(); + // This doesn't make any sense in a vnodes environment. + if (metadata.tokenMap.tokens(self).size() > 1) + { + logger.error("Invalid request to move(Token); This node has more than one token and cannot be moved thusly."); + throw new UnsupportedOperationException("This node has more than one token and cannot be moved thusly."); + } + + ClusterMetadataService.instance().commit(new PrepareMove(self, + Collections.singleton(newToken), + ClusterMetadataService.instance().placementProvider(), + true)); + InProgressSequences.finishInProgressSequences(self); + + if (logger.isDebugEnabled()) + logger.debug("Successfully moved to new token {}", StorageService.instance.getLocalTokens().iterator().next()); + } + +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapAndLeave.java b/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapAndLeave.java new file mode 100644 index 0000000000..f2d470556a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapAndLeave.java @@ -0,0 +1,367 @@ +/* + * 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.tcm.sequences; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.DynamicEndpointSnitch; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.utils.JVMStabilityInspector; +import org.apache.cassandra.utils.vint.VIntCoding; + +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_LEAVE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_LEAVE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_LEAVE; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.LEAVE; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; +import static org.apache.cassandra.tcm.sequences.SequenceState.error; + +public class UnbootstrapAndLeave extends MultiStepOperation +{ + private static final Logger logger = LoggerFactory.getLogger(UnbootstrapAndLeave.class); + public static final Serializer serializer = new Serializer(); + + public final LockedRanges.Key lockKey; + public final Transformation.Kind next; + + public final PrepareLeave.StartLeave startLeave; + public final PrepareLeave.MidLeave midLeave; + public final PrepareLeave.FinishLeave finishLeave; + private final LeaveStreams streams; + + public static UnbootstrapAndLeave newSequence(Epoch preparedAt, + LockedRanges.Key lockKey, + PrepareLeave.StartLeave startLeave, + PrepareLeave.MidLeave midLeave, + PrepareLeave.FinishLeave finishLeave, + LeaveStreams streams) + { + return new UnbootstrapAndLeave(preparedAt, + lockKey, + START_LEAVE, + startLeave, midLeave, finishLeave, + streams); + } + + /** + * Used by factory method for external callers and by Serializer + */ + @VisibleForTesting + UnbootstrapAndLeave(Epoch latestModification, + LockedRanges.Key lockKey, + Transformation.Kind next, + PrepareLeave.StartLeave startLeave, + PrepareLeave.MidLeave midLeave, + PrepareLeave.FinishLeave finishLeave, + LeaveStreams streams) + { + super(nextToIndex(next), latestModification); + this.lockKey = lockKey; + this.next = next; + this.startLeave = startLeave; + this.midLeave = midLeave; + this.finishLeave = finishLeave; + this.streams = streams; + } + + /** + * Used by advance to move forward in the sequence after execution + */ + private UnbootstrapAndLeave(UnbootstrapAndLeave current, Epoch latestModification) + { + super(current.idx + 1, latestModification); + this.next = indexToNext(current.idx + 1); + this.lockKey = current.lockKey; + this.startLeave = current.startLeave; + this.midLeave = current.midLeave; + this.finishLeave = current.finishLeave; + this.streams = current.streams; + } + + @Override + public Kind kind() + { + switch (streams.kind()) + { + case UNBOOTSTRAP: + return LEAVE; + case REMOVENODE: + return MultiStepOperation.Kind.REMOVE; + default: + throw new IllegalStateException("Invalid stream kind: "+streams.kind()); + } + } + + @Override + protected SequenceKey sequenceKey() + { + return startLeave.nodeId(); + } + + @Override + public MetadataSerializer keySerializer() + { + return NodeId.serializer; + } + + @Override + public Transformation.Kind nextStep() + { + return indexToNext(idx); + } + + @Override + public SequenceState executeNext() + { + switch (next) + { + case START_LEAVE: + try + { + DatabaseDescriptor.getSeverityDuringDecommission().ifPresent(DynamicEndpointSnitch::addSeverity); + ClusterMetadataService.instance().commit(startLeave); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return continuable(); + } + break; + case MID_LEAVE: + try + { + streams.execute(startLeave.nodeId(), + startLeave.delta(), + midLeave.delta(), + finishLeave.delta()); + ClusterMetadataService.instance().commit(midLeave); + } + catch (ExecutionException e) + { + StorageService.instance.markDecommissionFailed(); + JVMStabilityInspector.inspectThrowable(e); + logger.error("Error while decommissioning node: {}", e.getCause().getMessage()); + throw new RuntimeException("Error while decommissioning node: " + e.getCause().getMessage()); + } + catch (Throwable t) + { + logger.warn("Error committing midLeave", t); + JVMStabilityInspector.inspectThrowable(t); + return continuable(); + } + break; + case FINISH_LEAVE: + try + { + ClusterMetadataService.instance().commit(finishLeave); + StorageService.instance.clearTransientMode(); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return continuable(); + } + break; + default: + return error(new IllegalStateException("Can't proceed with leave from " + next)); + } + + return continuable(); + } + + @Override + public UnbootstrapAndLeave advance(Epoch waitUntilAcknowledged) + { + return new UnbootstrapAndLeave(this, waitUntilAcknowledged); + } + + @Override + public ProgressBarrier barrier() + { + ClusterMetadata metadata = ClusterMetadata.current(); + LockedRanges.AffectedRanges affectedRanges = metadata.lockedRanges.locked.get(lockKey); + Location location = metadata.directory.location(startLeave.nodeId()); + if (kind() == MultiStepOperation.Kind.REMOVE) + return new ProgressBarrier(latestModification, location, affectedRanges, (e) -> !e.equals(metadata.directory.endpoint(startLeave.nodeId()))); + else + return new ProgressBarrier(latestModification, location, affectedRanges); + } + + @Override + public ClusterMetadata.Transformer cancel(ClusterMetadata metadata) + { + DataPlacements placements = metadata.placements; + switch (next) + { + // need to undo MID_LEAVE and START_LEAVE, but PrepareLeave doesn't affect placement + case FINISH_LEAVE: + placements = midLeave.inverseDelta().apply(metadata.nextEpoch(), placements); + case MID_LEAVE: + case START_LEAVE: + placements = startLeave.inverseDelta().apply(metadata.nextEpoch(), placements); + break; + default: + throw new IllegalStateException("Can't revert leave from " + next); + } + LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey); + return metadata.transformer() + .with(placements) + .with(newLockedRanges) + .withNodeState(startLeave.nodeId(), NodeState.JOINED); + } + + @Override + public String status() + { + // Overridden to maintain compatibility with nodetool removenode output + return String.format("step: %s, streams: %s", next, streams.status()); + } + + private static int nextToIndex(Transformation.Kind next) + { + switch (next) + { + case START_LEAVE: + return 0; + case MID_LEAVE: + return 1; + case FINISH_LEAVE: + return 2; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", next, LEAVE)); + } + } + + private static Transformation.Kind indexToNext(int index) + { + switch (index) + { + case 0: + return START_LEAVE; + case 1: + return MID_LEAVE; + case 2: + return FINISH_LEAVE; + default: + throw new IllegalStateException(String.format("Step %s is invalid for sequence %s ", index, LEAVE)); + } + } + + @Override + public String toString() + { + return "UnbootstrapAndLeavePlan{" + + "lastModified=" + latestModification + + ", lockKey=" + lockKey + + ", startLeave=" + startLeave + + ", midLeave=" + midLeave + + ", finishLeave=" + finishLeave + + ", next=" + next + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UnbootstrapAndLeave that = (UnbootstrapAndLeave) o; + return next == that.next && + Objects.equals(startLeave, that.startLeave) && + Objects.equals(midLeave, that.midLeave) && + Objects.equals(finishLeave, that.finishLeave) && + Objects.equals(latestModification, that.latestModification) && + Objects.equals(lockKey, that.lockKey); + } + + @Override + public int hashCode() + { + return Objects.hash(startLeave, midLeave, finishLeave, latestModification, lockKey, next); + } + + public static class Serializer implements AsymmetricMetadataSerializer, UnbootstrapAndLeave> + { + public void serialize(MultiStepOperation t, DataOutputPlus out, Version version) throws IOException + { + UnbootstrapAndLeave plan = (UnbootstrapAndLeave) t; + + Epoch.serializer.serialize(plan.latestModification, out, version); + LockedRanges.Key.serializer.serialize(plan.lockKey, out, version); + VIntCoding.writeUnsignedVInt32(plan.next.ordinal(), out); + VIntCoding.writeUnsignedVInt32(plan.streams.kind().ordinal(), out); + + PrepareLeave.StartLeave.serializer.serialize(plan.startLeave, out, version); + PrepareLeave.MidLeave.serializer.serialize(plan.midLeave, out, version); + PrepareLeave.FinishLeave.serializer.serialize(plan.finishLeave, out, version); + } + + public UnbootstrapAndLeave deserialize(DataInputPlus in, Version version) throws IOException + { + Epoch barrier = Epoch.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + + Transformation.Kind next = Transformation.Kind.values()[VIntCoding.readUnsignedVInt32(in)]; + LeaveStreams.Kind streamKind = LeaveStreams.Kind.values()[VIntCoding.readUnsignedVInt32(in)]; + PrepareLeave.StartLeave startLeave = PrepareLeave.StartLeave.serializer.deserialize(in, version); + PrepareLeave.MidLeave midLeave = PrepareLeave.MidLeave.serializer.deserialize(in, version); + PrepareLeave.FinishLeave finishLeave = PrepareLeave.FinishLeave.serializer.deserialize(in, version); + + return new UnbootstrapAndLeave(barrier, lockKey, next, + startLeave, midLeave, finishLeave, + streamKind.supplier.get()); + } + + public long serializedSize(MultiStepOperation t, Version version) + { + UnbootstrapAndLeave plan = (UnbootstrapAndLeave) t; + long size = Epoch.serializer.serializedSize(plan.latestModification, version); + size += LockedRanges.Key.serializer.serializedSize(plan.lockKey, version); + + size += VIntCoding.computeVIntSize(plan.kind().ordinal()); + size += VIntCoding.computeVIntSize(plan.streams.kind().ordinal()); + size += PrepareLeave.StartLeave.serializer.serializedSize(plan.startLeave, version); + size += PrepareLeave.StartLeave.serializer.serializedSize(plan.midLeave, version); + size += PrepareLeave.StartLeave.serializer.serializedSize(plan.finishLeave, version); + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapStreams.java b/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapStreams.java new file mode 100644 index 0000000000..defba882c7 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/sequences/UnbootstrapStreams.java @@ -0,0 +1,233 @@ +/* + * 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.tcm.sequences; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.batchlog.BatchlogManager; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.SystemStrategy; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.streaming.StreamPlan; +import org.apache.cassandra.streaming.StreamState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.utils.concurrent.Future; + +public class UnbootstrapStreams implements LeaveStreams +{ + private static final Logger logger = LoggerFactory.getLogger(UnbootstrapStreams.class); + private final AtomicBoolean started = new AtomicBoolean(); + + public Kind kind() + { + return Kind.UNBOOTSTRAP; + } + + @Override + public void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave) throws ExecutionException, InterruptedException + { + MovementMap movements = movementMap(ClusterMetadata.current().directory.endpoint(leaving), + startLeave, + midLeave, + finishLeave); + movements.forEach((params, eps) -> logger.info("Unbootstrap movements: {}: {}", params, eps)); + started.set(true); + try + { + unbootstrap(Schema.instance.getNonLocalStrategyKeyspaces(), movements); + } + catch (ExecutionException e) + { + logger.error("Error while decommissioning node", e); + throw e; + } + } + + private static MovementMap movementMap(InetAddressAndPort leaving, PlacementDeltas startDelta, PlacementDeltas midDelta, PlacementDeltas finishDelta) + { + MovementMap.Builder allMovements = MovementMap.builder(); + // map of src->dest movements, keyed by replication settings. During unbootstrap, this will be used to construct + // a stream plan for each keyspace, based on their replication params. + finishDelta.forEach((params, delta) -> { + // no streaming for LocalStrategy and friends + if (SystemStrategy.class.isAssignableFrom(params.klass)) + return; + + // first identify ranges to be migrated off the leaving node + Map, Replica> oldReplicas = delta.writes.removals.get(leaving).byRange(); + + // next go through the additions to the write groups that will be applied during the + // first step of the plan. These represent the ranges moving to new replicas so in + // order to construct a streaming plan we can match these up with the corresponding + // removals to produce a src->dest mapping. + EndpointsByReplica.Builder movements = new EndpointsByReplica.Builder(); + RangesByEndpoint startWriteAdditions = startDelta.get(params).writes.additions; + startWriteAdditions.flattenValues() + .forEach(newReplica -> movements.put(oldReplicas.get(newReplica.range()), newReplica)); + // next, check if any replicas went from being transient to full, if so we need to stream to them; + Iterable removalReplicas = delta.writes.removals.flattenValues(); + for (Replica removal : removalReplicas) + { + if (removal.isTransient()) + { + Replica destination = midDelta.get(params).reads.additions.get(removal.endpoint()).byRange().get(removal.range()); + if (destination != null && destination.isFull()) + { + logger.info("Conversion from transient to full replica {} -> {}", removal, destination); + Replica source = oldReplicas.get(removal.range()); + movements.put(source, destination); + } + } + } + allMovements.put(params, movements.build()); + }); + return allMovements.build(); + } + + private static void unbootstrap(Keyspaces keyspaces, MovementMap movements) throws ExecutionException, InterruptedException + { + Supplier> startStreaming = prepareUnbootstrapStreaming(keyspaces, movements); + + StorageService.instance.repairPaxosForTopologyChange("decommission"); + logger.info("replaying batch log and streaming data to other nodes"); + // Start with BatchLog replay, which may create hints but no writes since this is no longer a valid endpoint. + Future batchlogReplay = BatchlogManager.instance.startBatchlogReplay(); + Future streamSuccess = startStreaming.get(); + + // Wait for batch log to complete before streaming hints. + logger.debug("waiting for batch log processing."); + batchlogReplay.get(); + + logger.info("streaming hints to other nodes"); + + Future hintsSuccess = StorageService.instance.streamHints(); + + // wait for the transfer runnables to signal the latch. + logger.debug("waiting for stream acks."); + streamSuccess.get(); + hintsSuccess.get(); + + logger.debug("stream acks all received."); + } + + private static Supplier> prepareUnbootstrapStreaming(Keyspaces keyspaces, + MovementMap movements) + { + // PrepareLeave transformation gives us a map of range movements for unbootstrap, keyed on replication settings. + // The movements themselves are maps of leavingReplica -> newReplica(s). Here we just "inflate" the outer + // map to a set of movements per-keyspace, duplicating where keyspaces share the same replication params + Map byKeyspace = + keyspaces.stream() + .collect(Collectors.toMap(k -> k.name, + k -> movements.get(k.params.replication))); + + return () -> streamRanges(byKeyspace); + } + + /** + * Send data to the endpoints that will be responsible for it in the future + * + * @param rangesToStreamByKeyspace keyspaces and data ranges with endpoints included for each + * @return async Future for whether stream was success + */ + private static Future streamRanges(Map rangesToStreamByKeyspace) + { + // First, we build a list of ranges to stream to each host, per table + Map sessionsToStreamByKeyspace = new HashMap<>(); + + for (Map.Entry entry : rangesToStreamByKeyspace.entrySet()) + { + String keyspace = entry.getKey(); + EndpointsByReplica rangesWithEndpoints = entry.getValue(); + + if (rangesWithEndpoints.isEmpty()) + continue; + + //Description is always Unbootstrap? Is that right? + Map>> transferredRangePerKeyspace = SystemKeyspace.getTransferredRanges("Unbootstrap", + keyspace, + ClusterMetadata.current().tokenMap.partitioner()); + RangesByEndpoint.Builder replicasPerEndpoint = new RangesByEndpoint.Builder(); + for (Map.Entry endPointEntry : rangesWithEndpoints.flattenEntries()) + { + Replica local = endPointEntry.getKey(); + Replica remote = endPointEntry.getValue(); + Set> transferredRanges = transferredRangePerKeyspace.get(remote.endpoint()); + if (transferredRanges != null && transferredRanges.contains(local.range())) + { + logger.debug("Skipping transferred range {} of keyspace {}, endpoint {}", local, keyspace, remote); + continue; + } + + replicasPerEndpoint.put(remote.endpoint(), remote.decorateSubrange(local.range())); + } + + sessionsToStreamByKeyspace.put(keyspace, replicasPerEndpoint.build()); + } + + StreamPlan streamPlan = new StreamPlan(StreamOperation.DECOMMISSION); + + // Vinculate StreamStateStore to current StreamPlan to update transferred ranges per StreamSession + streamPlan.listeners(StorageService.instance.streamStateStore()); + + for (Map.Entry entry : sessionsToStreamByKeyspace.entrySet()) + { + String keyspaceName = entry.getKey(); + RangesByEndpoint replicasPerEndpoint = entry.getValue(); + + for (Map.Entry rangesEntry : replicasPerEndpoint.asMap().entrySet()) + { + RangesAtEndpoint replicas = rangesEntry.getValue(); + InetAddressAndPort newEndpoint = rangesEntry.getKey(); + + // TODO each call to transferRanges re-flushes, this is potentially a lot of waste + streamPlan.transferRanges(newEndpoint, keyspaceName, replicas); + } + } + return streamPlan.execute(); + } + + // todo: add more details + public String status() + { + return "streams" + (started.get() ? "" : " not") + " started"; + } +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/AsymmetricMetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/AsymmetricMetadataSerializer.java new file mode 100644 index 0000000000..a499d155c1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/AsymmetricMetadataSerializer.java @@ -0,0 +1,53 @@ +/* + * 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.tcm.serialization; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +public interface AsymmetricMetadataSerializer +{ + /** + * Serialize the specified type into the specified DataOutputStream instance. + * + * @param t type that needs to be serialized + * @param out DataOutput into which serialization needs to happen. + * @throws IOException if serialization fails + */ + public void serialize(In t, DataOutputPlus out, Version version) throws IOException; + + /** + * Deserialize into the specified DataInputStream instance. + * @param in DataInput from which deserialization needs to happen. + * @param version protocol version + * @return the type that was deserialized + * @throws IOException if deserialization fails + */ + public Out deserialize(DataInputPlus in, Version version) throws IOException; + + + /** + * Calculate serialized size of object without actually serializing. + * @param t object to calculate serialized size + * @param version protocol version + * @return serialized size of object t + */ + public long serializedSize(In t, Version version); +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/MessageSerializers.java b/src/java/org/apache/cassandra/tcm/serialization/MessageSerializers.java new file mode 100644 index 0000000000..3eb361dc5f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/MessageSerializers.java @@ -0,0 +1,87 @@ +/* + * 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.tcm.serialization; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.migration.ClusterMetadataHolder; + +/** + * Provides IVersionedSerializers for internode messages where the payload includes + * elements of ClusterMetadata. Metadata elements are versioned seperately from + * MessagingService and the appropriate version is not established based on the + * peer receiving the messages, but is the lowest supported version of any member + * of the cluster. + */ +public class MessageSerializers +{ + public static IVersionedSerializer logStateSerializer() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.clusterMinVersion.serializationVersion == NodeVersion.CURRENT.serializationVersion) + return LogState.defaultMessageSerializer; + + assert !metadata.directory.clusterMinVersion.serializationVersion().equals(NodeVersion.CURRENT.serializationVersion()); + return LogState.messageSerializer(metadata.directory.clusterMinVersion.serializationVersion()); + } + + public static IVersionedSerializer commitResultSerializer() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.clusterMinVersion.serializationVersion == NodeVersion.CURRENT.serializationVersion) + return Commit.Result.defaultMessageSerializer; + + assert !metadata.directory.clusterMinVersion.serializationVersion().equals(NodeVersion.CURRENT.serializationVersion()); + return Commit.Result.messageSerializer(metadata.directory.clusterMinVersion.serializationVersion()); + } + + public static IVersionedSerializer commitSerializer() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.clusterMinVersion.serializationVersion == NodeVersion.CURRENT.serializationVersion) + return Commit.defaultMessageSerializer; + + assert !metadata.directory.clusterMinVersion.serializationVersion().equals(NodeVersion.CURRENT.serializationVersion()); + return Commit.messageSerializer(metadata.directory.clusterMinVersion.serializationVersion()); + } + + public static IVersionedSerializer replicationSerializer() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.clusterMinVersion.serializationVersion == NodeVersion.CURRENT.serializationVersion) + return Replication.defaultMessageSerializer; + + assert metadata.directory.clusterMinVersion.serializationVersion().isBefore(NodeVersion.CURRENT.serializationVersion()); + return Replication.messageSerializer(metadata.directory.clusterMinVersion.serializationVersion()); + } + + public static IVersionedSerializer metadataHolderSerializer() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null || metadata.directory.clusterMinVersion.serializationVersion == NodeVersion.CURRENT.serializationVersion) + return ClusterMetadataHolder.defaultMessageSerializer; + + assert !metadata.directory.clusterMinVersion.serializationVersion().equals(NodeVersion.CURRENT.serializationVersion()); + return ClusterMetadataHolder.messageSerializer(metadata.directory.clusterMinVersion.serializationVersion()); + } +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/MetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/MetadataSerializer.java new file mode 100644 index 0000000000..f0264af98f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/MetadataSerializer.java @@ -0,0 +1,22 @@ +/* + * 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.tcm.serialization; + +public interface MetadataSerializer extends AsymmetricMetadataSerializer +{ +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/PartitionerAwareMetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/PartitionerAwareMetadataSerializer.java new file mode 100644 index 0000000000..e414d1b7b6 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/PartitionerAwareMetadataSerializer.java @@ -0,0 +1,56 @@ +/* + * 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.tcm.serialization; + +import java.io.IOException; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +public interface PartitionerAwareMetadataSerializer +{ + /** + * Serialize the specified type into the specified DataOutputStream instance. + * + * @param t type that needs to be serialized + * @param out DataOutput into which serialization needs to happen. + * @throws IOException if serialization fails + */ + void serialize(T t, DataOutputPlus out, Version version) throws IOException; + + /** + * Deserialize into the specified DataInputStream instance. + * @param in DataInput from which deserialization needs to happen. + * @param partitioner The partitioner to use + * @param version protocol version + * @return the type that was deserialized + * @throws IOException if deserialization fails + */ + T deserialize(DataInputPlus in, IPartitioner partitioner, Version version) throws IOException; + + + /** + * Calculate serialized size of object without actually serializing. + * @param t object to calculate serialized size + * @param version protocol version + * @return serialized size of object t + */ + long serializedSize(T t, Version version); +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/UDTAndFunctionsAwareMetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/UDTAndFunctionsAwareMetadataSerializer.java new file mode 100644 index 0000000000..896e44af4d --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/UDTAndFunctionsAwareMetadataSerializer.java @@ -0,0 +1,63 @@ +/* + * 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.tcm.serialization; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; + +/** + * This can of course be used *by* a MetadataSerializer (e.g. by KeyspaceMetadata.Serialize at the top level) + * but it cannot be used *as* a MetadataSerializer, as more than just the input bytes + version are required + */ +public interface UDTAndFunctionsAwareMetadataSerializer +{ + + /** + * Serialize the specified type into the specified DataOutputStream instance. + * + * @param t type that needs to be serialized + * @param out DataOutput into which serialization needs to happen. + * @throws IOException if serialization fails + */ + public void serialize(T t, DataOutputPlus out, Version version) throws IOException; + + /** + * Deserialize into the specified DataInputStream instance. + * @param in DataInput from which deserialization needs to happen. + * @param types collection of UDTs required to deserialize the instance + * @param functions collection of user defined functions required to deserialize the instance + * @param version protocol version + * @return the type that was deserialized + * @throws IOException if deserialization fails + */ + public T deserialize(DataInputPlus in, Types types, UserFunctions functions, Version version) throws IOException; + + + /** + * Calculate serialized size of object without actually serializing. + * @param t object to calculate serialized size + * @param version protocol version + * @return serialized size of object t + */ + public long serializedSize(T t, Version version); +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/UDTAwareMetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/UDTAwareMetadataSerializer.java new file mode 100644 index 0000000000..655ff950ec --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/UDTAwareMetadataSerializer.java @@ -0,0 +1,61 @@ +/* + * 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.tcm.serialization; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.Types; + +/** + * This can of course be used *by* a MetadataSerializer (e.g. by KeyspaceMetadata.Serialize at the top level) + * but it cannot be used *as* a MetadataSerializer, as more than just the input bytes + version are required + */ +public interface UDTAwareMetadataSerializer +{ + + /** + * Serialize the specified type into the specified DataOutputStream instance. + * + * @param t type that needs to be serialized + * @param out DataOutput into which serialization needs to happen. + * @throws IOException if serialization fails + */ + public void serialize(T t, DataOutputPlus out, Version version) throws IOException; + + /** + * Deserialize into the specified DataInputStream instance. + * @param in DataInput from which deserialization needs to happen. + * @param types collection of UDTs required to deserialize the instance + * @param version protocol version + * @return the type that was deserialized + * @throws IOException if deserialization fails + */ + public T deserialize(DataInputPlus in, Types types, Version version) throws IOException; + + + /** + * Calculate serialized size of object without actually serializing. + * @param t object to calculate serialized size + * @param version protocol version + * @return serialized size of object t + */ + public long serializedSize(T t, Version version); +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/VerboseMetadataSerializer.java b/src/java/org/apache/cassandra/tcm/serialization/VerboseMetadataSerializer.java new file mode 100644 index 0000000000..c8b3fdc7e1 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/VerboseMetadataSerializer.java @@ -0,0 +1,53 @@ +/* + * 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.tcm.serialization; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.vint.VIntCoding; + +public class VerboseMetadataSerializer +{ + public static void serialize(AsymmetricMetadataSerializer base, + In t, + DataOutputPlus out, + Version version) throws IOException + { + out.writeUnsignedVInt32(version.asInt()); + base.serialize(t, out, version); + } + + public static Out deserialize(AsymmetricMetadataSerializer base, + DataInputPlus in) throws IOException + { + int x = in.readUnsignedVInt32(); + Version v = Version.fromInt(x); + return base.deserialize(in, v); + } + + public static long serializedSize(AsymmetricMetadataSerializer base, + In t, + Version version) + { + return VIntCoding.computeUnsignedVIntSize(version.asInt()) + + base.serializedSize(t, version); + } +} diff --git a/src/java/org/apache/cassandra/tcm/serialization/Version.java b/src/java/org/apache/cassandra/tcm/serialization/Version.java new file mode 100644 index 0000000000..ae2afb6f00 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/serialization/Version.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.serialization; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeVersion; + +public enum Version +{ + OLD(-1), + V0(0), + /** + * - Moved Partitioner in ClusterMetadata serializer to be the first field + * - Added a counter to Directory serializer to keep track of NodeIds + */ + V1(1), + /** + * - Added version to PlacementForRange serializer + * - Serialize MemtableParams when serializing TableParams + */ + V2(2), + + UNKNOWN(Integer.MAX_VALUE); + + private static Map values = new HashMap<>(); + static + { + for (Version v : values()) + values.put(v.version, v); + } + + private final int version; + Version(int version) + { + this.version = version; + } + + /** + * Minimum serialization version known to all nodes in the cluster. + */ + public static Version minCommonSerializationVersion() + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata != null) + return metadata.directory.clusterMinVersion.serializationVersion(); + return NodeVersion.CURRENT.serializationVersion(); + + } + + public int asInt() + { + return version; + } + + public boolean equals(Version other) + { + return version == other.version; + } + + public boolean isAtLeast(Version other) + { + return version >= other.version; + } + + public boolean isBefore(Version other) + { + return version < other.version; + } + + public static Version fromInt(int i) + { + Version v = values.get(i); + if (v != null) + return v; + + throw new IllegalArgumentException("Unsupported metadata version (" + i + ")"); + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/AlterSchema.java b/src/java/org/apache/cassandra/tcm/transformations/AlterSchema.java new file mode 100644 index 0000000000..3751934224 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/AlterSchema.java @@ -0,0 +1,280 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.cassandra.exceptions.AlreadyExistsException; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaProvider; +import org.apache.cassandra.schema.SchemaTransformation; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.Tables; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.JVMStabilityInspector; + +import static org.apache.cassandra.exceptions.ExceptionCode.ALREADY_EXISTS; +import static org.apache.cassandra.exceptions.ExceptionCode.CONFIG_ERROR; +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.exceptions.ExceptionCode.SERVER_ERROR; +import static org.apache.cassandra.exceptions.ExceptionCode.SYNTAX_ERROR; + +public class AlterSchema implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(AlterSchema.class); + public static final Serializer serializer = new Serializer(); + + public final SchemaTransformation schemaTransformation; + protected final SchemaProvider schemaProvider; + + public AlterSchema(SchemaTransformation schemaTransformation, + SchemaProvider schemaProvider) + { + this.schemaTransformation = schemaTransformation; + this.schemaProvider = schemaProvider; + } + + @Override + public Kind kind() + { + return Kind.SCHEMA_CHANGE; + } + + @Override + public final Result execute(ClusterMetadata prev) + { + Keyspaces newKeyspaces; + try + { + // Applying the schema transformation may produce client warnings. If this is being executed by a follower + // of the cluster metadata log, there is no client or ClientState, so warning collection is a no-op. + // When a DDL statement is received from an actual client, the transformation is checked for validation + // and warnings are captured at that point, before being submitted to the CMS. + // If the coordinator is a CMS member, then this method will be called as part of committing to the metadata + // log. In this case, there is a connected client and associated ClientState, so to avoid duplicate warnings + // pause capture and resume after in applying the schema change. + schemaTransformation.enterExecution(); + + // Guard against an invalid SchemaTransformation supplying a TableMetadata with a future epoch + newKeyspaces = schemaTransformation.apply(prev); + newKeyspaces.forEach(ksm -> { + ksm.tables.forEach(tm -> { + if (tm.epoch.isAfter(prev.nextEpoch())) + throw new InvalidRequestException(String.format("Invalid schema transformation. " + + "Resultant epoch for table metadata of %s.%s (%d) " + + "is greater than for cluster metadata (%d)", + ksm.name, tm.name, tm.epoch.getEpoch(), + prev.nextEpoch().getEpoch())); + }); + }); + } + catch (AlreadyExistsException t) + { + return new Rejected(ALREADY_EXISTS, t.getMessage()); + } + catch (ConfigurationException t) + { + return new Rejected(CONFIG_ERROR, t.getMessage()); + } + catch (InvalidRequestException t) + { + return new Rejected(INVALID, t.getMessage()); + } + catch (SyntaxException t) + { + return new Rejected(SYNTAX_ERROR, t.getMessage()); + } + catch (Throwable t) + { + JVMStabilityInspector.inspectThrowable(t); + return new Rejected(SERVER_ERROR, t.getMessage()); + } + finally + { + schemaTransformation.exitExecution(); + } + + Keyspaces.KeyspacesDiff diff = Keyspaces.diff(prev.schema.getKeyspaces(), newKeyspaces); + + // Used to ensure that any new or modified TableMetadata has the correct epoch + Epoch nextEpoch = prev.nextEpoch(); + + // Used to determine whether this schema change impacts data placements in any way. + // If so, then reject the change if there are data movement operations inflight, i.e. if any ranges are locked. + // If not, or if no ranges are locked then the change is permitted and placements recalculated as part of this + // transformation. + // Impact on data placements is determined as: + // * Any new keyspace configured with a previously unused set of replication params + // * Any existing keyspace with an altered set of replication params + // * Dropping all keyspaces with a specific set of replication params + Set affectsPlacements = new HashSet<>(); + Map> keyspacesByReplication = groupByReplication(prev.schema.getKeyspaces()); + + // Scan dropped keyspaces to check if any existing replication scheme will become unused after this change + Map> intendedToDrop = groupByReplication(diff.dropped); + intendedToDrop.forEach((replication, keyspaces) -> { + if (keyspaces.containsAll(keyspacesByReplication.get(replication))) + affectsPlacements.addAll(keyspaces); + }); + + // Scan new keyspaces to check for any new replication schemes and to ensure that the metadata of new tables + // in those keyspaces has the correct epoch + for (KeyspaceMetadata newKSM : diff.created) + { + if (!keyspacesByReplication.containsKey(newKSM.params.replication)) + affectsPlacements.add(newKSM); + + Tables tables = Tables.of(normaliseEpochs(nextEpoch, newKSM.tables.stream())); + newKeyspaces = newKeyspaces.withAddedOrUpdated(newKSM.withSwapped(tables)); + } + + // Scan modified keyspaces to check for replication changes and to ensure that any modified table metadata + // has the correct epoch + for (KeyspaceMetadata.KeyspaceDiff alteredKSM : diff.altered) + { + if (!alteredKSM.before.params.replication.equals(alteredKSM.after.params.replication)) + affectsPlacements.add(alteredKSM.before); + + Tables tables = Tables.of(alteredKSM.after.tables); + for (TableMetadata created : normaliseEpochs(nextEpoch, alteredKSM.tables.created.stream())) + tables = tables.withSwapped(created); + + for (TableMetadata altered : normaliseEpochs(nextEpoch, alteredKSM.tables.altered.stream().map(altered -> altered.after))) + tables = tables.withSwapped(altered); + newKeyspaces = newKeyspaces.withAddedOrUpdated(alteredKSM.after.withSwapped(tables)); + } + + // Changes which affect placement (i.e. new, removed or altered replication settings) are not allowed if there + // are ongoing range movements, including node replacements and partial joins (nodes in write survey mode). + if (!affectsPlacements.isEmpty()) + { + logger.debug("Schema change affects data placements, relevant keyspaces: {}", affectsPlacements); + if (!prev.lockedRanges.locked.isEmpty()) + return new Rejected(INVALID, + String.format("The requested schema changes cannot be executed as they conflict " + + "with ongoing range movements. The changes for keyspaces %s are blocked " + + "by the locked ranges %s", + affectsPlacements.stream().map(k -> k.name).collect(Collectors.joining(",", "[", "]")), + prev.lockedRanges.locked)); + + } + + DistributedSchema snapshotAfter = new DistributedSchema(newKeyspaces); + ClusterMetadata.Transformer next = prev.transformer().with(snapshotAfter); + if (!affectsPlacements.isEmpty()) + { + // state.schema is a DistributedSchema, so doesn't include local keyspaces. If we don't explicitly include those + // here, their placements won't be calculated, effectively dropping them from the new versioned state + Keyspaces allKeyspaces = prev.schema.getKeyspaces().withAddedOrReplaced(snapshotAfter.getKeyspaces()); + DataPlacements calculatedPlacements = ClusterMetadataService.instance() + .placementProvider() + .calculatePlacements(prev.nextEpoch(), prev.tokenMap.toRanges(), prev, allKeyspaces); + + DataPlacements.Builder newPlacementsBuilder = DataPlacements.builder(calculatedPlacements.size()); + calculatedPlacements.forEach((params, newPlacement) -> { + DataPlacement previousPlacement = prev.placements.get(params); + // Preserve placement versioning that has resulted from natural application where possible + if (previousPlacement.equals(newPlacement)) + newPlacementsBuilder.with(params, previousPlacement); + else + newPlacementsBuilder.with(params, newPlacement); + }); + next = next.with(newPlacementsBuilder.build()); + } + + return Transformation.success(next, LockedRanges.AffectedRanges.EMPTY); + } + + private static Map> groupByReplication(Keyspaces keyspaces) + { + Map> byReplication = new HashMap<>(); + for (KeyspaceMetadata ksm : keyspaces) + { + ReplicationParams params = ksm.params.replication; + Set forReplication = byReplication.computeIfAbsent(params, p -> new HashSet<>()); + forReplication.add(ksm); + } + return byReplication; + } + + private static Iterable normaliseEpochs(Epoch nextEpoch, Stream tables) + { + return tables.map(tm -> tm.epoch.is(nextEpoch) + ? tm + : tm.unbuild().epoch(nextEpoch).build()) + .collect(Collectors.toList()); + } + + + static class Serializer implements AsymmetricMetadataSerializer + { + @Override + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + SchemaTransformation.serializer.serialize(((AlterSchema) t).schemaTransformation, out, version); + } + + @Override + public AlterSchema deserialize(DataInputPlus in, Version version) throws IOException + { + return new AlterSchema(SchemaTransformation.serializer.deserialize(in, version), + Schema.instance); + + } + + @Override + public long serializedSize(Transformation t, Version version) + { + return SchemaTransformation.serializer.serializedSize(((AlterSchema) t).schemaTransformation, version); + } + } + + @Override + public String toString() + { + return "AlterSchema{" + + "schemaTransformation=" + schemaTransformation + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/ApplyPlacementDeltas.java b/src/java/org/apache/cassandra/tcm/transformations/ApplyPlacementDeltas.java new file mode 100644 index 0000000000..2f55f712ba --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/ApplyPlacementDeltas.java @@ -0,0 +1,148 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.exceptions.ExceptionCode; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; + +public abstract class ApplyPlacementDeltas implements Transformation +{ + protected final NodeId nodeId; + protected final PlacementDeltas delta; + protected final LockedRanges.Key lockKey; + protected final boolean unlock; + + ApplyPlacementDeltas(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey, boolean unlock) + { + this.nodeId = nodeId; + this.delta = delta; + this.lockKey = lockKey; + this.unlock = unlock; + } + + public abstract Kind kind(); + public abstract ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer); + + public PlacementDeltas inverseDelta() + { + return delta.invert(); + } + + public PlacementDeltas delta() + { + return delta; + } + + @Override + public final Result execute(ClusterMetadata prev) + { + if (!prev.inProgressSequences.contains(nodeId())) + return new Rejected(ExceptionCode.INVALID, "Can't find an in-progress sequence for this operation"); + + if (prev.inProgressSequences.get(nodeId()).nextStep() != kind()) + return new Rejected(ExceptionCode.INVALID, String.format("Can't commit sequenced operations out of order. Expected %s, but got %s", prev.inProgressSequences.get(nodeId()).nextStep(), kind())); + + ClusterMetadata.Transformer next = prev.transformer(); + + if (!delta.isEmpty()) + next = next.with(delta.apply(prev.nextEpoch(), prev.placements)); + + next = transform(prev, next); + + if (unlock) + next = next.with(prev.lockedRanges.unlock(lockKey)); + + return Transformation.success(next, prev.lockedRanges.locked.get(lockKey)); + } + + @Override + public String toString() + { + return getClass().getSimpleName() + '{' + + "id=" + nodeId + + ", kind=" + kind() + + ", delta=" + delta + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof ApplyPlacementDeltas)) return false; + ApplyPlacementDeltas that = (ApplyPlacementDeltas) o; + return unlock == that.unlock && + kind().equals(that.kind()) && + Objects.equals(nodeId, that.nodeId) && + Objects.equals(delta, that.delta) && + Objects.equals(lockKey, that.lockKey); + } + + @Override + public int hashCode() + { + return Objects.hash(nodeId, kind(), delta, lockKey, unlock); + } + + public NodeId nodeId() + { + return nodeId; + } + + abstract static class SerializerBase implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + ApplyPlacementDeltas change = (T) t; + NodeId.serializer.serialize(change.nodeId, out, version); + PlacementDeltas.serializer.serialize(change.delta, out, version); + LockedRanges.Key.serializer.serialize(change.lockKey, out, version); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + PlacementDeltas delta = PlacementDeltas.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + return construct(nodeId, delta, lockKey); + } + + public long serializedSize(Transformation t, Version version) + { + ApplyPlacementDeltas change = (T) t; + + return NodeId.serializer.serializedSize(change.nodeId, version) + + PlacementDeltas.serializer.serializedSize(change.delta, version) + + LockedRanges.Key.serializer.serializedSize(change.lockKey, version); + } + + abstract T construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey); + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/Assassinate.java b/src/java/org/apache/cassandra/tcm/transformations/Assassinate.java new file mode 100644 index 0000000000..0cee0f627f --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/Assassinate.java @@ -0,0 +1,133 @@ +/* + * 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.tcm.transformations; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataKey; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; + +public class Assassinate extends PrepareLeave +{ + public static final Serializer serializer = new Serializer() + { + @Override + public Assassinate construct(NodeId leaving, boolean force, PlacementProvider placementProvider, LeaveStreams.Kind streamKind) + { + return new Assassinate(leaving, placementProvider); + } + }; + + public Assassinate(NodeId leaving, PlacementProvider placementProvider) + { + super(leaving, true, placementProvider, LeaveStreams.Kind.ASSASSINATE); + } + + /** + * Force-remove endpoint from token map. + * + * @param endpoint endpoint to remove + */ + public static void assassinateEndpoint(InetAddressAndPort endpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + // Gossip implementation of assassinate was a no-op. Preserving this behaviour. + if (!metadata.directory.isRegistered(endpoint)) + return; + + NodeId nodeId = metadata.directory.peerId(endpoint); + ClusterMetadataService.instance().commit(new Assassinate(nodeId, + ClusterMetadataService.instance().placementProvider())); + } + + @Override + public Kind kind() + { + return Kind.ASSASSINATE; + } + + public String toString() + { + return "Assassinate{" + + "leaving=" + leaving + + '}'; + } + + @Override + public Result execute(ClusterMetadata prev) + { + Result result = super.execute(prev); + if (result.isRejected()) + return result; + + Success success = result.success(); + ClusterMetadata metadata = success.metadata; + Epoch forceEpoch = metadata.epoch; + metadata = success.metadata.forceEpoch(prev.epoch); + + UnbootstrapAndLeave plan = (UnbootstrapAndLeave) metadata.inProgressSequences.get(nodeId()); + + ImmutableSet.Builder modifiedKeys = ImmutableSet.builder(); + + success = plan.startLeave.execute(metadata).success(); + metadata = success.metadata.forceEpoch(prev.epoch); + modifiedKeys.addAll(success.affectedMetadata); + + success = plan.midLeave.execute(metadata).success(); + metadata = success.metadata.forceEpoch(prev.epoch); + modifiedKeys.addAll(success.affectedMetadata); + + success = plan.finishLeave.execute(metadata).success(); + metadata = success.metadata; + modifiedKeys.addAll(success.affectedMetadata); + + metadata = metadata.forceEpoch(forceEpoch); + + return new Success(metadata, LockedRanges.AffectedRanges.EMPTY, modifiedKeys.build()); + } + + public static LeaveStreams LEAVE_STREAMS = new LeaveStreams() + { + @Override + public void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave) + { + + } + + @Override + public Kind kind() + { + return Kind.ASSASSINATE; + } + + public String status() + { + return "streaming finished"; + } + }; +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/CancelInProgressSequence.java b/src/java/org/apache/cassandra/tcm/transformations/CancelInProgressSequence.java new file mode 100644 index 0000000000..fa736f4a53 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/CancelInProgressSequence.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.transformations; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class CancelInProgressSequence implements Transformation +{ + public static final Serializer serializer = new Serializer(); + + private final NodeId nodeId; + + public CancelInProgressSequence(NodeId nodeId) + { + this.nodeId = nodeId; + } + + @Override + public Kind kind() + { + return Kind.CANCEL_SEQUENCE; + } + + @Override + public Result execute(ClusterMetadata prev) + { + MultiStepOperation sequence = prev.inProgressSequences.get(nodeId); + if (null == sequence) + return new Rejected(INVALID, String.format("No in-progress sequence found for node id %s at epoch %s", + nodeId, prev.epoch)); + + ClusterMetadata.Transformer transformer = sequence.cancel(prev) + .with(prev.inProgressSequences.without(nodeId)); + return Transformation.success(transformer, LockedRanges.AffectedRanges.EMPTY); + } + + @Override + public String toString() + { + return "CancelInProgressSequence{" + + "nodeId=" + nodeId + + '}'; + } + + public static final class Serializer implements AsymmetricMetadataSerializer + { + @Override + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t instanceof CancelInProgressSequence; + CancelInProgressSequence cancel = (CancelInProgressSequence) t; + NodeId.serializer.serialize(cancel.nodeId, out, version); + } + + @Override + public CancelInProgressSequence deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + return new CancelInProgressSequence(nodeId); + } + + @Override + public long serializedSize(Transformation t, Version version) + { + assert t instanceof CancelInProgressSequence; + return NodeId.serializer.serializedSize(((CancelInProgressSequence)t).nodeId, version); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/CustomTransformation.java b/src/java/org/apache/cassandra/tcm/transformations/CustomTransformation.java new file mode 100644 index 0000000000..e7d7d3cac8 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/CustomTransformation.java @@ -0,0 +1,264 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.*; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.extensions.ExtensionKey; +import org.apache.cassandra.tcm.extensions.IntValue; +import org.apache.cassandra.tcm.extensions.StringValue; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.sequences.LockedRanges; + +public class CustomTransformation implements Transformation +{ + public static Serializer serializer = new Serializer(); + private final String extension; + private final Transformation child; + + private static Map> extensions = new HashMap<>(); + + static + { + registerExtension(PokeString.NAME, new PokeString.TransformSerializer()); + registerExtension(PokeInt.NAME, new PokeInt.TransformSerializer()); + } + + public CustomTransformation(String extension, Transformation child) + { + this.extension = extension; + this.child = child; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CustomTransformation that = (CustomTransformation) o; + return Objects.equals(child, that.child); + } + + @Override + public int hashCode() + { + return Objects.hash(child); + } + + public Transformation child() + { + return child; + } + + public static CustomTransformation make(String v) + { + return new CustomTransformation(PokeString.NAME, new PokeString(v)); + } + + public static CustomTransformation make(int v) + { + return new CustomTransformation(PokeInt.NAME, new PokeInt(v)); + } + + public static AsymmetricMetadataSerializer getSerializer(String name) + { + assert extensions.containsKey(name) : String.format("Unknown extension %s. Available extensions %s.", name, extensions.keySet()); + return extensions.get(name); + } + + public static void registerExtension(String name, AsymmetricMetadataSerializer callback) + { + extensions.put(name, callback); + } + + public static void unregisterExtension(String name) + { + extensions.remove(name); + } + + public static void clearExtensions() + { + extensions.clear(); + } + + public Kind kind() + { + return Kind.CUSTOM; + } + + public Result execute(ClusterMetadata prev) + { + return child.execute(prev); + } + + public String toString() + { + return "CustomTransformation{" + + "extension='" + extension + '\'' + + ", child=" + child + + '}'; + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + private Serializer() {} + + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + CustomTransformation s = (CustomTransformation) t; + out.writeInt(s.extension.length()); + out.writeBytes(s.extension); + extensions.get(s.extension).serialize(s.child, out, version); + } + + public CustomTransformation deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + byte[] bytes = new byte[size]; + in.readFully(bytes); + String extension = new String(bytes); + return new CustomTransformation(extension, getSerializer(extension).deserialize(in, version)); + } + + public long serializedSize(Transformation t, Version version) + { + CustomTransformation s = (CustomTransformation) t; + return Integer.BYTES + s.extension.length() + getSerializer(s.extension).serializedSize(s.child, version); + } + } + + public static class PokeString implements Transformation + { + // how to identify and obtain a serializer for instances of this Transformation + public static final String NAME = PokeString.class.getName(); + + // This Transformation will insert/overwrite a value in the map ClusterMetadata maintains for arbitrary values. + // To do this, it must provide a key to identify the value and a wrapper type for serialization. + public static final ExtensionKey METADATA_KEY = new ExtensionKey<>(NAME, StringValue.class); + + public final String str; + + public PokeString(String str) + { + this.str = str; + } + + public Kind kind() + { + return Kind.CUSTOM; + } + + public Result execute(ClusterMetadata prev) + { + StringValue value = StringValue.create(str); + return Transformation.success(prev.transformer().with(METADATA_KEY, value), LockedRanges.AffectedRanges.EMPTY); + } + + public String toString() + { + return String.format("String{'%s'}", str); + } + + public static class TransformSerializer implements AsymmetricMetadataSerializer + { + private TransformSerializer() {} + + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + PokeString s = (PokeString) t; + out.writeInt(s.str.length()); + out.writeBytes(s.str); + } + + public PokeString deserialize(DataInputPlus in, Version version) throws IOException + { + int size = in.readInt(); + byte[] bytes = new byte[size]; + in.readFully(bytes); + return new PokeString(new String(bytes)); + } + + public long serializedSize(Transformation t, Version version) + { + return Integer.BYTES + ((PokeString) t).str.length(); + } + } + } + + public static class PokeInt implements Transformation + { + // how to identify and obtain a serializer for instances of this Transformation + public static final String NAME = PokeInt.class.getName(); + + // This Transformation will insert/overwrite a value in the map ClusterMetadata maintains for arbitrary values. + // To do this, it must provide a key to identify the value and a wrapper type for serialization. + public static final ExtensionKey METADATA_KEY = new ExtensionKey<>(NAME, IntValue.class); + + public final int v; + + public PokeInt(int v) + { + this.v = v; + } + + public Kind kind() + { + return Kind.CUSTOM; + } + + public Result execute(ClusterMetadata prev) + { + IntValue value = IntValue.create(v); + return Transformation.success(prev.transformer().with(METADATA_KEY, value), LockedRanges.AffectedRanges.EMPTY); + } + + public String toString() + { + return String.format("int{%d}", v); + } + + public static class TransformSerializer implements AsymmetricMetadataSerializer + { + private TransformSerializer() {} + + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + PokeInt s = (PokeInt) t; + out.writeInt(s.v); + } + + public PokeInt deserialize(DataInputPlus in, Version version) throws IOException + { + int v = in.readInt(); + return new PokeInt(v); + } + + public long serializedSize(Transformation t, Version version) + { + return Integer.BYTES; + } + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/ForceSnapshot.java b/src/java/org/apache/cassandra/tcm/transformations/ForceSnapshot.java new file mode 100644 index 0000000000..be9fde80db --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/ForceSnapshot.java @@ -0,0 +1,81 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MetadataKeys; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +/** + * Inferred ForceSnapshot transformation. When we receive LogState, it may happen that we receive a base state, + * in which case should apply snapshot, even if we skip multiple epochs while applying it. This transformation + * is not getting appended into base table, and is only used for forcing state that does not immediately + * supercede the current one. + */ +public class ForceSnapshot implements Transformation +{ + protected final ClusterMetadata baseState; + + public ForceSnapshot(ClusterMetadata baseState) + { + this.baseState = baseState; + } + + public Kind kind() + { + return Kind.FORCE_SNAPSHOT; + } + + public Result execute(ClusterMetadata metadata) + { + return new Success(baseState, LockedRanges.AffectedRanges.EMPTY, MetadataKeys.CORE_METADATA); + } + + public String toString() + { + return "ForceSnapshot{" + + "epoch=" + baseState.epoch + + '}'; + } + + public static final AsymmetricMetadataSerializer serializer = new AsymmetricMetadataSerializer() + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + ClusterMetadata.serializer.serialize(((ForceSnapshot) t).baseState, out, version); + } + + public ForceSnapshot deserialize(DataInputPlus in, Version version) throws IOException + { + return new ForceSnapshot(ClusterMetadata.serializer.deserialize(in, version)); + } + + public long serializedSize(Transformation t, Version version) + { + return ClusterMetadata.serializer.serializedSize(((ForceSnapshot) t).baseState, version); + } + }; +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/PrepareJoin.java b/src/java/org/apache/cassandra/tcm/transformations/PrepareJoin.java new file mode 100644 index 0000000000..662f96eabc --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/PrepareJoin.java @@ -0,0 +1,355 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +/** + * Create a plan for adding a new node and bootstrapping it, then start to execute that plan. + * Creating the plan involves adding the joining node's tokens to the current tokenmap and generating the set of + * required operations to be applied to existing data placements. + * Specifically, this involves splitting the ranges containing the new node's tokens and adding the new node as a + * write replica for those ranges. After the new node has completed streaming for bootstrap, it is made a read + * replica for those ranges. + * + * For example, if we start with a (subset of a) ring where A has token 100, B has token 200 and C has token 300 + * with RF=2 then both the read and write placements will contain: + * + * (0, 100] : {A,B} + * (100, 200] : {B,C} + * + * If we then begin bootstrap X with token 150, the first step is to range split the existing ranges in line without + * changing any ownership. At this point, both the read and write placements will contain: + * + * (0, 100] : {A,B} + * (100, 150] : {B,C} + * (150, 200] : {B,C} + * + * Next, the new node is added to the write groups for the ranges it is acquiring. After this step, the read placement + * is unchanged, while the write placement will contain: + * + * (0, 100] : {A,B,X} + * (100, 150] : {B,C,X} + * (150, 200] : {B,C} + * + * Once X completes bootstrapping, it is added to the corresponding read groups, replacing nodes which are no longer + * replicas for those ranges. Now both the read and write placements will contain: + * + * (0, 100] : {A,X} + * (100, 150] : {X,B} + * (150, 200] : {B,C} + * + * Currently, we do not automatically clean up unowned ranges after bootstrap, so C will still hold data for + * (100, 150] on disk even though it is no longer a replica for that range. + */ +public class PrepareJoin implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(PrepareJoin.class); + + public static final Serializer serializer = new Serializer() + { + public PrepareJoin construct(NodeId nodeId, Set tokens, PlacementProvider placementProvider, boolean joinTokenRing, boolean streamData) + { + return new PrepareJoin(nodeId, tokens, placementProvider, joinTokenRing, streamData); + } + }; + + protected final NodeId nodeId; + protected final Set tokens; + protected final PlacementProvider placementProvider; + protected final boolean joinTokenRing; + protected final boolean streamData; + + public PrepareJoin(NodeId nodeId, + Set tokens, + PlacementProvider placementProvider, + boolean joinTokenRing, + boolean streamData) + { + this.nodeId = nodeId; + this.tokens = tokens; + this.placementProvider = placementProvider; + this.joinTokenRing = joinTokenRing; + this.streamData = streamData; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_JOIN; + } + + public NodeId nodeId() + { + return nodeId; + } + + private static final Set ALLOWED_STATES = ImmutableSet.of(NodeState.REGISTERED, NodeState.LEFT); + + @Override + public Result execute(ClusterMetadata prev) + { + if (!ALLOWED_STATES.contains(prev.directory.peerState(nodeId))) + return new Rejected(INVALID, String.format("Rejecting this plan as the node %s is in state %s", + nodeId, prev.directory.peerState(nodeId))); + + PlacementTransitionPlan transitionPlan = placementProvider.planForJoin(prev, nodeId, tokens, prev.schema.getKeyspaces()); + + LockedRanges.AffectedRanges rangesToLock = transitionPlan.affectedRanges(); + LockedRanges.Key alreadyLockedBy = prev.lockedRanges.intersects(rangesToLock); + if (!alreadyLockedBy.equals(LockedRanges.NOT_LOCKED)) + { + return new Rejected(INVALID, String.format("Rejecting this plan as it interacts with a range locked by %s (locked: %s, new: %s)", + alreadyLockedBy, prev.lockedRanges, rangesToLock)); + } + + LockedRanges.Key lockKey = LockedRanges.keyFor(prev.nextEpoch()); + StartJoin startJoin = new StartJoin(nodeId, transitionPlan.addToWrites(), lockKey); + MidJoin midJoin = new MidJoin(nodeId, transitionPlan.moveReads(), lockKey); + FinishJoin finishJoin = new FinishJoin(nodeId, tokens, transitionPlan.removeFromWrites(), lockKey); + + BootstrapAndJoin plan = BootstrapAndJoin.newSequence(prev.nextEpoch(), + lockKey, + transitionPlan.toSplit, + startJoin, midJoin, finishJoin, + joinTokenRing, streamData); + + LockedRanges newLockedRanges = prev.lockedRanges.lock(lockKey, rangesToLock); + DataPlacements startingPlacements = transitionPlan.toSplit.apply(prev.nextEpoch(), prev.placements); + ClusterMetadata.Transformer proposed = prev.transformer() + .with(newLockedRanges) + .with(startingPlacements) + .with(prev.inProgressSequences.with(nodeId, plan)); + + return Transformation.success(proposed, rangesToLock); + } + + public static abstract class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + T transformation = (T) t; + NodeId.serializer.serialize(transformation.nodeId, out, version); + out.writeInt(transformation.tokens.size()); + for(Token token : transformation.tokens) + Token.metadataSerializer.serialize(token, out, version); + out.writeBoolean(transformation.joinTokenRing); + out.writeBoolean(transformation.streamData); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId id = NodeId.serializer.deserialize(in, version); + int numTokens = in.readInt(); + Set tokens = new HashSet<>(numTokens); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < numTokens; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + boolean joinTokenRing = in.readBoolean(); + boolean streamData = in.readBoolean(); + return construct(id, tokens, ClusterMetadataService.instance().placementProvider(), joinTokenRing, streamData); + } + + public long serializedSize(Transformation t, Version version) + { + T transformation = (T) t; + long size = NodeId.serializer.serializedSize(transformation.nodeId, version); + size += TypeSizes.INT_SIZE; + for (Token token : transformation.tokens) + size += Token.metadataSerializer.serializedSize(token, version); + size += TypeSizes.BOOL_SIZE * 2; + return size; + } + + public abstract T construct(NodeId nodeId, + Set tokens, + PlacementProvider placementProvider, + boolean joinTokenRing, + boolean streamData); + + } + + public String toString() + { + return "PrepareJoin{" + + "nodeId=" + nodeId + + ", tokens=" + tokens + + ", joinTokenRing=" + joinTokenRing + + ", streamData=" + streamData + + '}'; + } + + public static class StartJoin extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public StartJoin(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.START_JOIN; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.withNodeState(nodeId, NodeState.BOOTSTRAPPING) + .with(prev.inProgressSequences.with(nodeId, (BootstrapAndJoin plan) -> plan.advance(prev.nextEpoch()))); + } + + public static final class Serializer extends SerializerBase + { + StartJoin construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new StartJoin(nodeId, delta, lockKey); + } + } + } + + public static class MidJoin extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public MidJoin(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.MID_JOIN; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.with(prev.inProgressSequences.with(nodeId, (BootstrapAndJoin plan) -> plan.advance(prev.nextEpoch()))); + } + + public static final class Serializer extends SerializerBase + { + MidJoin construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new MidJoin(nodeId, delta, lockKey); + } + } + } + + public static class FinishJoin extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + public final Set tokens; + + public FinishJoin(NodeId nodeId, Set tokens, PlacementDeltas delta, LockedRanges.Key unlockKey) + { + super(nodeId, delta, unlockKey, true); + this.tokens = tokens; + } + + @Override + public Kind kind() + { + return Kind.FINISH_JOIN; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.join(nodeId) + .proposeToken(nodeId, tokens) + .addToRackAndDC(nodeId) + .with(prev.inProgressSequences.without(nodeId)); + } + + public static final class Serializer extends SerializerBase + { + @Override + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + super.serialize(t, out, version); + Set tokens = ((FinishJoin)t).tokens; + out.writeUnsignedVInt32(tokens.size()); + for (Token token : tokens) + Token.metadataSerializer.serialize(token, out, version); + } + + @Override + public FinishJoin deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + PlacementDeltas delta = PlacementDeltas.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + int numTokens = in.readUnsignedVInt32(); + Set tokens = new HashSet<>(); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < numTokens; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + return new FinishJoin(nodeId, tokens, delta, lockKey); + } + + @Override + public long serializedSize(Transformation t, Version version) + { + long size = super.serializedSize(t, version); + Set tokens = ((FinishJoin)t).tokens; + size += TypeSizes.sizeofUnsignedVInt(tokens.size()); + for (Token token : tokens) + size += Token.metadataSerializer.serializedSize(token, version); + return size; + } + + FinishJoin construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + throw new IllegalStateException(); + } + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java b/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java new file mode 100644 index 0000000000..c19edc393d --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java @@ -0,0 +1,320 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.NetworkTopologyStrategy; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class PrepareLeave implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(PrepareLeave.class); + public static final Serializer serializer = new Serializer() + { + @Override + public PrepareLeave construct(NodeId leaving, boolean force, PlacementProvider placementProvider, LeaveStreams.Kind streamKind) + { + return new PrepareLeave(leaving, force, placementProvider, streamKind); + } + }; + + protected final NodeId leaving; + protected final boolean force; + protected final PlacementProvider placementProvider; + protected final LeaveStreams.Kind streamKind; + + public PrepareLeave(NodeId leaving, boolean force, PlacementProvider placementProvider, LeaveStreams.Kind streamKind) + { + this.leaving = leaving; + this.force = force; + this.placementProvider = placementProvider; + this.streamKind = streamKind; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_LEAVE; + } + + public NodeId nodeId() + { + return leaving; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (prev.isCMSMember(prev.directory.endpoint(leaving))) + return new Rejected(INVALID, String.format("Rejecting this plan as the node %s is still a part of CMS.", leaving)); + + if (prev.directory.peerState(leaving) != NodeState.JOINED) + return new Rejected(INVALID, String.format("Rejecting this plan as the node %s is in state %s", leaving, prev.directory.peerState(leaving))); + + ClusterMetadata proposed = prev.transformer().proposeRemoveNode(leaving).build().metadata; + + if (!force && !validateReplicationForDecommission(proposed)) + return new Rejected(INVALID, "Not enough live nodes to maintain replication factor after decommission."); + + if (proposed.directory.isEmpty()) + return new Rejected(INVALID, "No peers registered, at least local node should be"); + + PlacementTransitionPlan transitionPlan = placementProvider.planForDecommission(prev, + leaving, + prev.schema.getKeyspaces()); + + LockedRanges.AffectedRanges rangesToLock = transitionPlan.affectedRanges(); + LockedRanges.Key alreadyLockedBy = prev.lockedRanges.intersects(rangesToLock); + if (!alreadyLockedBy.equals(LockedRanges.NOT_LOCKED)) + { + return new Rejected(INVALID, String.format("Rejecting this plan as it interacts with a range locked by %s (locked: %s, new: %s)", + alreadyLockedBy, prev.lockedRanges, rangesToLock)); + } + + PlacementDeltas startDelta = transitionPlan.addToWrites(); + PlacementDeltas midDelta = transitionPlan.moveReads(); + PlacementDeltas finishDelta = transitionPlan.removeFromWrites(); + + LockedRanges.Key unlockKey = LockedRanges.keyFor(proposed.epoch); + + StartLeave start = new StartLeave(leaving, startDelta, unlockKey); + MidLeave mid = new MidLeave(leaving, midDelta, unlockKey); + FinishLeave leave = new FinishLeave(leaving, finishDelta, unlockKey); + + UnbootstrapAndLeave plan = UnbootstrapAndLeave.newSequence(prev.nextEpoch(), + unlockKey, + start, mid, leave, + streamKind.supplier.get()); + + // note: we throw away the state with the leaving node's tokens removed. It's only + // used to produce the operation plan. + ClusterMetadata.Transformer next = prev.transformer() + .with(prev.lockedRanges.lock(unlockKey, rangesToLock)) + .with(prev.inProgressSequences.with(leaving, plan)); + return Transformation.success(next, rangesToLock); + } + + private boolean validateReplicationForDecommission(ClusterMetadata proposed) + { + String dc = proposed.directory.location(leaving).datacenter; + int rf, numNodes; + for (KeyspaceMetadata ksm : proposed.schema.getKeyspaces()) + { + if (ksm.replicationStrategy instanceof NetworkTopologyStrategy) + { + NetworkTopologyStrategy strategy = (NetworkTopologyStrategy) ksm.replicationStrategy; + rf = strategy.getReplicationFactor(dc).allReplicas; + numNodes = joinedNodeCount(proposed.directory, proposed.directory.allDatacenterEndpoints().get(dc)); + + if (numNodes <= rf) + { + logger.warn("Not enough live nodes to maintain replication factor for keyspace {}. " + + "Replication factor in {} is {}, live nodes = {}. " + + "Perform a forceful decommission to ignore.", ksm, dc, rf, numNodes); + return false; + } + } + else if (ksm.params.replication.isMeta()) + { + // TODO: usually we should not allow decommissioning of CMS node + // from what i understand this is not necessarily decommissioning of the cms node; every node has this ks now + continue; + } + else + { + numNodes = joinedNodeCount(proposed.directory, proposed.directory.allAddresses()); + rf = ksm.replicationStrategy.getReplicationFactor().allReplicas; + if (numNodes <= rf) + { + logger.warn("Not enough live nodes to maintain replication factor in keyspace " + + ksm + " (RF = " + rf + ", N = " + numNodes + ")." + + " Perform a forceful decommission to ignore."); + return false; + } + } + } + return true; + } + + private static int joinedNodeCount(Directory directory, Collection endpoints) + { + return (int)endpoints.stream().filter(i -> directory.peerState(i) == NodeState.JOINED).count(); + } + + public static abstract class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + T transformation = (T) t; + NodeId.serializer.serialize(transformation.leaving, out, version); + out.writeBoolean(transformation.force); + out.writeUTF(transformation.streamKind.toString()); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId id = NodeId.serializer.deserialize(in, version); + boolean force = in.readBoolean(); + LeaveStreams.Kind streamsKind = LeaveStreams.Kind.valueOf(in.readUTF()); + + return construct(id, + force, + ClusterMetadataService.instance().placementProvider(), + streamsKind); + } + + public long serializedSize(Transformation t, Version version) + { + T transformation = (T) t; + return NodeId.serializer.serializedSize(transformation.leaving, version) + + TypeSizes.sizeof(transformation.force) + + TypeSizes.sizeof(transformation.streamKind.toString()); + } + + public abstract T construct(NodeId leaving, boolean force, PlacementProvider placementProvider, LeaveStreams.Kind streamKind); + + } + + @Override + public String toString() + { + return "PrepareLeave{" + + "leaving=" + leaving + + ", force=" + force + + '}'; + } + + public static class StartLeave extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public StartLeave(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.START_LEAVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer + .with(prev.inProgressSequences.with(nodeId, (UnbootstrapAndLeave plan) -> plan.advance(prev.nextEpoch()))) + .withNodeState(nodeId, NodeState.LEAVING); + } + + public static final class Serializer extends SerializerBase + { + StartLeave construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new StartLeave(nodeId, delta, lockKey); + } + } + } + + public static class MidLeave extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public MidLeave(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.MID_LEAVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.with(prev.inProgressSequences.with(nodeId, (plan) -> plan.advance(prev.nextEpoch()))); + } + + public static final class Serializer extends SerializerBase + { + MidLeave construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new MidLeave(nodeId, delta, lockKey); + } + } + } + + public static class FinishLeave extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public FinishLeave(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, true); + } + + @Override + public Kind kind() + { + return Kind.FINISH_LEAVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.left(nodeId) + .with(prev.inProgressSequences.without(nodeId)); + } + + public static class Serializer extends SerializerBase + { + FinishLeave construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new FinishLeave(nodeId, delta, lockKey); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/PrepareMove.java b/src/java/org/apache/cassandra/tcm/transformations/PrepareMove.java new file mode 100644 index 0000000000..71fc3bf667 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/PrepareMove.java @@ -0,0 +1,300 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.Move; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class PrepareMove implements Transformation +{ + public static final Serializer serializer = new Serializer(); + + protected final NodeId nodeId; + protected final Set tokens; + protected final PlacementProvider placementProvider; + protected final boolean streamData; + + public PrepareMove(NodeId nodeId, + Set tokens, + PlacementProvider placementProvider, + boolean streamData) + { + this.nodeId = nodeId; + this.tokens = tokens; + this.placementProvider = placementProvider; + this.streamData = streamData; + } + + @Override + public String toString() + { + return "PrepareMove{" + + "nodeId=" + nodeId + + ", tokens=" + tokens + + ", streamData=" + streamData + + '}'; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_MOVE; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (prev.directory.peerState(nodeId) != NodeState.JOINED) + return new Rejected(INVALID, String.format("Rejecting this plan as the node %s is in state %s", nodeId, prev.directory.peerState(nodeId))); + + PlacementTransitionPlan transitionPlan = placementProvider.planForMove(prev, nodeId, tokens, prev.schema.getKeyspaces()); + LockedRanges.AffectedRanges rangesToLock = transitionPlan.affectedRanges(); + LockedRanges.Key alreadyLockedBy = prev.lockedRanges.intersects(rangesToLock); + + if (!alreadyLockedBy.equals(LockedRanges.NOT_LOCKED)) + { + return new Rejected(INVALID, String.format("Rejecting this plan as it interacts with a range locked by %s (locked: %s, new: %s)", + alreadyLockedBy, prev.lockedRanges, rangesToLock)); + } + + LockedRanges.Key lockKey = LockedRanges.keyFor(prev.nextEpoch()); + StartMove startMove = new StartMove(nodeId, transitionPlan.addToWrites(), lockKey); + MidMove midMove = new MidMove(nodeId, transitionPlan.moveReads(), lockKey); + FinishMove finishMove = new FinishMove(nodeId, tokens, transitionPlan.removeFromWrites(), lockKey); + + Move sequence = Move.newSequence(prev.nextEpoch(), + lockKey, + tokens, + transitionPlan.toSplit, + startMove, + midMove, + finishMove, + false); + + return Transformation.success(prev.transformer() + .withNodeState(nodeId, NodeState.MOVING) + .with(prev.lockedRanges.lock(lockKey, rangesToLock)) + .with(transitionPlan.toSplit.apply(prev.nextEpoch(), prev.placements)) + .with(prev.inProgressSequences.with(nodeId, sequence)), + rangesToLock); + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + T transformation = (T) t; + NodeId.serializer.serialize(transformation.nodeId, out, version); + out.writeInt(transformation.tokens.size()); + for(Token token : transformation.tokens) + Token.metadataSerializer.serialize(token, out, version); + out.writeBoolean(transformation.streamData); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId id = NodeId.serializer.deserialize(in, version); + int numTokens = in.readInt(); + Set tokens = new HashSet<>(numTokens); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < numTokens; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + boolean streamData = in.readBoolean(); + return construct(id, tokens, ClusterMetadataService.instance().placementProvider(), streamData); + } + + public long serializedSize(Transformation t, Version version) + { + T transformation = (T) t; + long size = NodeId.serializer.serializedSize(transformation.nodeId, version); + size += TypeSizes.INT_SIZE; + for (Token token : transformation.tokens) + size += Token.metadataSerializer.serializedSize(token, version); + size += TypeSizes.BOOL_SIZE; + return size; + } + + public T construct(NodeId nodeId, + Set tokens, + PlacementProvider placementProvider, + boolean streamData) + { + return (T) new PrepareMove(nodeId, tokens, placementProvider, streamData); + } + + } + + public static class StartMove extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + @VisibleForTesting + public StartMove(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.START_MOVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.with(prev.inProgressSequences.with(nodeId, (plan) -> plan.advance(prev.nextEpoch()))); + } + + public static final class Serializer extends SerializerBase + { + StartMove construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new PrepareMove.StartMove(nodeId, delta, lockKey); + } + } + } + + public static class MidMove extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + @VisibleForTesting + public MidMove(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, false); + } + + @Override + public Kind kind() + { + return Kind.MID_MOVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.with(prev.inProgressSequences.with(nodeId, (plan) -> plan.advance(prev.nextEpoch()))); + } + + public static final class Serializer extends SerializerBase + { + MidMove construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new PrepareMove.MidMove(nodeId, delta, lockKey); + } + } + } + + public static class FinishMove extends ApplyPlacementDeltas + { + public static final Serializer serializer = new Serializer(); + + public final Collection newTokens; + + @VisibleForTesting + public FinishMove(NodeId nodeId, Collection newTokens, PlacementDeltas delta, LockedRanges.Key lockKey) + { + super(nodeId, delta, lockKey, true); + this.newTokens = newTokens; + } + + @Override + public Kind kind() + { + return Kind.FINISH_MOVE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.moveTokens(nodeId, newTokens) + .join(nodeId) + .with(prev.inProgressSequences.without(nodeId)); + } + + public static final class Serializer extends SerializerBase + { + @Override + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + super.serialize(t, out, version); + Collection tokens = ((FinishMove)t).newTokens; + out.writeUnsignedVInt32(tokens.size()); + for (Token token : tokens) + Token.metadataSerializer.serialize(token, out, version); + } + + public FinishMove deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + PlacementDeltas delta = PlacementDeltas.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + int numTokens = in.readUnsignedVInt32(); + List tokens = new ArrayList<>(); + IPartitioner partitioner = ClusterMetadata.current().partitioner; + for (int i = 0; i < numTokens; i++) + tokens.add(Token.metadataSerializer.deserialize(in, partitioner, version)); + + return new FinishMove(nodeId, tokens, delta, lockKey); + } + + public long serializedSize(Transformation t, Version version) + { + long size = super.serializedSize(t, version); + Collection tokens = ((FinishMove)t).newTokens; + size += TypeSizes.sizeofUnsignedVInt(tokens.size()); + for (Token token : tokens) + size += Token.metadataSerializer.serializedSize(token, version); + return size; + } + + FinishMove construct(NodeId nodeId, PlacementDeltas delta, LockedRanges.Key lockKey) + { + throw new IllegalStateException(); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/PrepareReplace.java b/src/java/org/apache/cassandra/tcm/transformations/PrepareReplace.java new file mode 100644 index 0000000000..d1bcdb7518 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/PrepareReplace.java @@ -0,0 +1,386 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class PrepareReplace implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(PrepareReplace.class); + + public static final Serializer serializer = new Serializer(); + + private final NodeId replaced; + private final NodeId replacement; + private final PlacementProvider placementProvider; + private final boolean joinTokenRing; + private final boolean streamData; + + public PrepareReplace(NodeId replaced, + NodeId replacement, + PlacementProvider placementProvider, + boolean joinTokenRing, + boolean streamData) + { + this.replaced = replaced; + this.replacement = replacement; + this.placementProvider = placementProvider; + this.joinTokenRing = joinTokenRing; + this.streamData = streamData; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_REPLACE; + } + + public NodeId replacement() + { + return replacement; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (prev.directory.peerState(replaced) != NodeState.JOINED) + return new Rejected(INVALID, String.format("Rejecting this plan as the replaced node %s is in state %s", replaced, prev.directory.peerState(replaced))); + + if (prev.directory.peerState(replacement) != NodeState.REGISTERED) + return new Rejected(INVALID, String.format("Rejecting this plan as the replacement node %s is in state %s", replacement, prev.directory.peerState(replacement))); + + LockedRanges.Key unlockKey = LockedRanges.keyFor(prev.nextEpoch()); + LockedRanges lockedRanges = prev.lockedRanges; + + PlacementTransitionPlan transitionPlan = placementProvider.planForReplacement(prev, + replaced, + replacement, + prev.schema.getKeyspaces()); + PlacementDeltas.Builder addNewNodeToWrites = PlacementDeltas.builder(); + PlacementDeltas.Builder addNewNodeToReads = PlacementDeltas.builder(); + PlacementDeltas.Builder removeOldNodeFromWrites = PlacementDeltas.builder(); + + // Only addition of the new node to the write groups is done as a consequence of the first transformation. Adding the new + // node to the various read groups is deferred until the second transformation, after bootstrap. Also, track which ranges + // are going to be affected by this operation (i.e. which will be the "pending" ranges for the new node. If the + // plan is accepted those ranges will be locked to prevent other plans submitted later from interacting with the + // same ranges. + LockedRanges.AffectedRangesBuilder affectedRanges = LockedRanges.AffectedRanges.builder(); + transitionPlan.toMaximal.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + addNewNodeToWrites.put(replication, delta.onlyWrites().onlyAdditions()); + addNewNodeToReads.put(replication, delta.onlyReads()); + }); + + transitionPlan.toFinal.forEach((replication, delta) -> { + delta.reads.additions.flattenValues().forEach(r -> affectedRanges.add(replication, r.range())); + addNewNodeToReads.put(replication, delta.onlyReads()); + removeOldNodeFromWrites.put(replication, delta.onlyWrites().onlyRemovals()); + }); + + LockedRanges.AffectedRanges rangesToLock = affectedRanges.build(); + LockedRanges.Key alreadyLockedBy = lockedRanges.intersects(rangesToLock); + + if (!alreadyLockedBy.equals(LockedRanges.NOT_LOCKED)) + { + return new Rejected(INVALID, String.format("Rejecting this plan as it interacts with a range locked by %s (locked: %s, new: %s)", + alreadyLockedBy, lockedRanges, rangesToLock)); + } + + StartReplace start = new StartReplace(replaced, replacement, addNewNodeToWrites.build(), unlockKey); + MidReplace mid = new MidReplace(replaced, replacement, addNewNodeToReads.build(), unlockKey); + FinishReplace finish = new FinishReplace(replaced, replacement, removeOldNodeFromWrites.build(), unlockKey); + + Set tokens = new HashSet<>(prev.tokenMap.tokens(replaced)); + BootstrapAndReplace plan = BootstrapAndReplace.newSequence(prev.nextEpoch(), + unlockKey, + tokens, + start, mid, finish, + joinTokenRing, streamData); + + LockedRanges newLockedRanges = lockedRanges.lock(unlockKey, rangesToLock); + ClusterMetadata.Transformer proposed = prev.transformer() + .with(newLockedRanges) + .with(prev.inProgressSequences.with(replacement(), plan)); + logger.info("Node {} is replacing {}, tokens {}", prev.directory.endpoint(replacement), prev.directory.endpoint(replaced), prev.tokenMap.tokens(replaced)); + return Transformation.success(proposed, rangesToLock); + } + + @Override + public String toString() + { + return "PrepareReplace{" + + "replaced=" + replaced + + ", replacement=" + replacement + + ", joinTokenRing=" + joinTokenRing + + ", streamData=" + streamData + + '}'; + } + + public static final class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t instanceof PrepareReplace; + PrepareReplace transformation = (PrepareReplace) t; + NodeId.serializer.serialize(transformation.replaced, out, version); + NodeId.serializer.serialize(transformation.replacement, out, version); + out.writeBoolean(transformation.joinTokenRing); + out.writeBoolean(transformation.streamData); + } + + public PrepareReplace deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId replaced = NodeId.serializer.deserialize(in, version); + NodeId replacement = NodeId.serializer.deserialize(in, version); + boolean joinTokenRing = in.readBoolean(); + boolean streamData = in.readBoolean(); + return new PrepareReplace(replaced, + replacement, + ClusterMetadataService.instance().placementProvider(), + joinTokenRing, + streamData); + } + + public long serializedSize(Transformation t, Version version) + { + assert t instanceof PrepareReplace; + PrepareReplace transformation = (PrepareReplace) t; + return NodeId.serializer.serializedSize(transformation.replaced, version) + + NodeId.serializer.serializedSize(transformation.replacement, version) + + (TypeSizes.BOOL_SIZE * 2); + } + } + + public static abstract class BaseReplaceTransformation extends ApplyPlacementDeltas + { + protected final NodeId replaced; + + public BaseReplaceTransformation(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key unlockKey, boolean unlock) + { + super(replacement, delta, unlockKey, unlock); + this.replaced = replaced; + } + + public NodeId replacement() + { + return nodeId; + } + + public NodeId replaced() + { + return replaced; + } + } + + // Analogous to ApplyPlacementDeltas.SerializerBase. The difference is that classes which extend + // BaseReplaceTransformation have 2 NodeIds, whereas the equivalents from PrepareJoin and PrepareLeave only have a single id + // TODO we can probably make the hierarchy of abstract/concrete transformations and associated serializers a bit less convoluted + public abstract static class SerializerBase implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + BaseReplaceTransformation change = (T) t; + NodeId.serializer.serialize(change.replaced(), out, version); + NodeId.serializer.serialize(change.replacement(), out, version); + PlacementDeltas.serializer.serialize(change.delta, out, version); + LockedRanges.Key.serializer.serialize(change.lockKey, out, version); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId replaced = NodeId.serializer.deserialize(in, version); + NodeId replacement = NodeId.serializer.deserialize(in, version); + PlacementDeltas delta = PlacementDeltas.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + return construct(replaced, replacement, delta, lockKey); + } + + public long serializedSize(Transformation t, Version version) + { + BaseReplaceTransformation change = (T) t; + + return NodeId.serializer.serializedSize(change.replaced(), version) + + NodeId.serializer.serializedSize(change.replacement(), version) + + PlacementDeltas.serializer.serializedSize(change.delta, version) + + LockedRanges.Key.serializer.serializedSize(change.lockKey, version); + } + + abstract T construct(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key lockKey); + } + + // This is functionally identical to StartJoin. They only exist as distinct transformations for clarity. + public static class StartReplace extends BaseReplaceTransformation + { + public static final Serializer serializer = new Serializer(); + + public StartReplace(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key unlockKey) + { + super(replaced, replacement, delta, unlockKey, false); + } + + @Override + public Kind kind() + { + return Kind.START_REPLACE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.withNodeState(replacement(), NodeState.BOOT_REPLACING) + .with(prev.inProgressSequences.with(nodeId, (BootstrapAndReplace plan) -> plan.advance(prev.nextEpoch()))); + } + + @Override + public String toString() + { + return "StartReplace{" + + ", delta=" + delta + + ", lockKey=" + lockKey + + ", unlock=" + unlock + + ", replaced=" + replaced + + ", replacement=" + replacement() + + '}'; + } + + public static final class Serializer extends PrepareReplace.SerializerBase + { + @Override + public StartReplace construct(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key unlockKey) + { + return new StartReplace(replaced, replacement, delta, unlockKey); + } + } + } + + public static class MidReplace extends BaseReplaceTransformation + { + public static final Serializer serializer = new Serializer(); + + public MidReplace(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key unlockKey) + { + super(replaced, replacement, delta, unlockKey, false); + } + + @Override + public Kind kind() + { + return Kind.MID_REPLACE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer + .with(prev.inProgressSequences.with(nodeId, (plan) -> plan.advance(prev.nextEpoch()))); + } + + @Override + public String toString() + { + return "MidReplace{" + + ", delta=" + delta + + ", lockKey=" + lockKey + + ", unlock=" + unlock + + ", replaced=" + replaced + + ", replacement=" + replacement() + + '}'; + } + + public static final class Serializer extends PrepareReplace.SerializerBase + { + @Override + MidReplace construct(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new MidReplace(replaced, replacement, delta, lockKey); + } + } + } + + public static class FinishReplace extends BaseReplaceTransformation + { + public static final Serializer serializer = new Serializer(); + + public FinishReplace(NodeId replaced, + NodeId replacement, + PlacementDeltas delta, + LockedRanges.Key unlockKey) + { + super(replaced, replacement, delta, unlockKey, true); + } + + @Override + public Kind kind() + { + return Kind.FINISH_REPLACE; + } + + @Override + public ClusterMetadata.Transformer transform(ClusterMetadata prev, ClusterMetadata.Transformer transformer) + { + return transformer.replaced(replaced, replacement()) + .with(prev.inProgressSequences.without(nodeId)); + } + + @Override + public String toString() + { + return "FinishReplace{" + + "replaced=" + replaced + + ", replacement=" + replacement() + + ", delta=" + delta + + ", unlockKey=" + lockKey + + '}'; + } + + public static final class Serializer extends PrepareReplace.SerializerBase + { + @Override + FinishReplace construct(NodeId replaced, NodeId replacement, PlacementDeltas delta, LockedRanges.Key lockKey) + { + return new FinishReplace(replaced, replacement, delta, lockKey); + } + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/Register.java b/src/java/org/apache/cassandra/tcm/transformations/Register.java new file mode 100644 index 0000000000..014d549fde --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/Register.java @@ -0,0 +1,216 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.IEndpointSnitch; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class Register implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(Register.class); + public static final Serializer serializer = new Serializer(); + + private final NodeAddresses addresses; + private final Location location; + private final NodeVersion version; + + public Register(NodeAddresses addresses, Location location, NodeVersion version) + { + this.location = location; + this.version = version; + this.addresses = addresses; + } + + @Override + public Kind kind() + { + return Kind.REGISTER; + } + + @Override + public Result execute(ClusterMetadata prev) + { + for (Map.Entry entry : prev.directory.addresses.entrySet()) + { + NodeAddresses existingAddresses = entry.getValue(); + if (addresses.conflictsWith(existingAddresses)) + return new Rejected(INVALID, String.format("New addresses %s conflicts with existing node %s with addresses %s", addresses, entry.getKey(), existingAddresses)); + } + + ClusterMetadata.Transformer next = prev.transformer() + .register(addresses, location, version); + return Transformation.success(next, LockedRanges.AffectedRanges.EMPTY); + } + + public static NodeId maybeRegister() + { + return register(false); + } + + @VisibleForTesting + public static NodeId register(NodeAddresses nodeAddresses) + { + return register(nodeAddresses, NodeVersion.CURRENT); + } + + @VisibleForTesting + public static NodeId register(NodeAddresses nodeAddresses, NodeVersion nodeVersion) + { + IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); + Location location = new Location(snitch.getLocalDatacenter(), snitch.getLocalRack()); + + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(nodeAddresses.broadcastAddress); + if (nodeId == null || metadata.directory.peerState(nodeId) == NodeState.LEFT) + { + if (nodeId != null) + ClusterMetadataService.instance() + .commit(new Unregister(nodeId)); + nodeId = ClusterMetadataService.instance() + .commit(new Register(nodeAddresses, location, nodeVersion)) + .directory + .peerId(nodeAddresses.broadcastAddress); + } + else + { + throw new IllegalStateException(String.format("A node with address %s already exists, cancelling join. Use cassandra.replace_address if you want to replace this node.", nodeAddresses.broadcastAddress)); + } + + logger.info("Registering with endpoint {}", nodeAddresses.broadcastAddress); + return nodeId; + } + + private static NodeId register(boolean force) + { + // Try to recover node ID from the system keyspace + UUID localHostId = SystemKeyspace.getLocalHostId(); + Directory directory = ClusterMetadata.current().directory; + if (force || localHostId == null) + { + NodeId nodeId = register(NodeAddresses.current()); + localHostId = nodeId.toUUID(); + SystemKeyspace.setLocalHostId(localHostId); + logger.info("New node ID obtained {}, (Note: This should happen exactly once per node)", localHostId); + return nodeId; + } + else if (NodeId.isValidNodeId(localHostId) && directory.peerIds().contains(NodeId.fromUUID(localHostId))) + { + return NodeId.fromUUID(localHostId); + } + else + { + NodeId nodeId = directory.peerId(FBUtilities.getBroadcastAddressAndPort()); + NodeVersion dirVersion = directory.version(nodeId); + + // If this is a node in the process of upgrading, update the host id in the system.local table + // TODO: when constructing the initial cluster metadata for upgrade, we include a mapping from + // NodeId to the old HostId. We will need to use this lookup to map between the two for + // hint delivery immediately following an upgrade. + if (dirVersion == null || !dirVersion.isUpgraded()) + { + if (directory.hostId(nodeId).equals(localHostId)) + { + SystemKeyspace.setLocalHostId(nodeId.toUUID()); + logger.info("Updated local HostId from pre-upgrade version {} to the one which was pre-registered " + + "during initial cluster metadata conversion {}", localHostId, nodeId.toUUID()); + } + else + { + throw new RuntimeException("HostId read from local system table does not match the one recorded " + + "for this endpoint during initial cluster metadata conversion. " + + String.format("Endpoint: %s, NodeId: %s, Recorded: %s, Local: %s", + FBUtilities.getBroadcastAddressAndPort(), + nodeId, + directory.hostId(nodeId), + localHostId)); + } + } + else + { + logger.info("Local id was already registered, retaining: {}", localHostId); + } + return nodeId; + } + } + + @Override + public String toString() + { + return "Register{" + + "addresses=" + addresses + + ", location=" + location + + ", version=" + version + + '}'; + } + + static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t instanceof Register; + Register register = (Register)t; + NodeAddresses.serializer.serialize(register.addresses, out, version); + Location.serializer.serialize(register.location, out, version); + NodeVersion.serializer.serialize(register.version, out, version); + } + + public Register deserialize(DataInputPlus in, Version version) throws IOException + { + NodeAddresses addresses = NodeAddresses.serializer.deserialize(in, version); + Location location = Location.serializer.deserialize(in, version); + NodeVersion nodeVersion = NodeVersion.serializer.deserialize(in, version); + return new Register(addresses, location, nodeVersion); + } + + public long serializedSize(Transformation t, Version version) + { + assert t instanceof Register; + Register register = (Register) t; + return NodeAddresses.serializer.serializedSize(register.addresses, version) + + Location.serializer.serializedSize(register.location, version) + + NodeVersion.serializer.serializedSize(register.version, version); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/SealPeriod.java b/src/java/org/apache/cassandra/tcm/transformations/SealPeriod.java new file mode 100644 index 0000000000..5e8222ebf4 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/SealPeriod.java @@ -0,0 +1,100 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +/** + * Period is an entity that is completely transparent to the users of CMS, and is a consequence of a fact that + * LWTs only work on a single partition. It would be unwise to hold all epochs in a single partition, as it + * will eventually get extremely large. At the same time, we can not use Epoch as a primary key (even though + * IF NOT EXISTS queries would technically work for append puposes), since it would make log scans much more + * expensive. + * + * Another reason for having periods is that replaying an entire log for a freshly starting log in an old + * cluster can be very expensive, so in such cases the node can be caught up using a snapshot serving as a base + * state and a small number of entries instead. + * + * Transformation that seals the period and requests local state to take a snapshot. Snapshot taking is an + * asynchonous action, and we generally do not rely on the fact snapshot is, in fact going to be + * there all the time. Snapshots are used as a performance optimization. + */ +public class SealPeriod implements Transformation +{ + public static final Serializer serializer = new Serializer(); + + public static SealPeriod instance = new SealPeriod(); + + private SealPeriod(){} + + @Override + public Kind kind() + { + return Kind.SEAL_PERIOD; + } + + @Override + public boolean allowDuringUpgrades() + { + return true; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (prev.lastInPeriod) + return new Rejected(INVALID, "Have just sealed this period"); + + return Transformation.success(prev.transformer(true), LockedRanges.AffectedRanges.EMPTY); + } + + static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t == instance; + } + + public SealPeriod deserialize(DataInputPlus in, Version version) throws IOException + { + return instance; + } + + public long serializedSize(Transformation t, Version version) + { + return 0; + } + } + + @Override + public String toString() + { + return "SealPeriod{}"; + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/Startup.java b/src/java/org/apache/cassandra/tcm/transformations/Startup.java new file mode 100644 index 0000000000..b26cc3655c --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/Startup.java @@ -0,0 +1,157 @@ +/* + * 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.tcm.transformations; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class Startup implements Transformation +{ + public static final Serializer serializer = new Serializer(); + private final NodeId nodeId; + private final NodeVersion nodeVersion; + private final NodeAddresses addresses; + + public Startup(NodeId nodeId, + NodeAddresses addresses, + NodeVersion nodeVersion) + { + this.nodeId = nodeId; + this.nodeVersion = nodeVersion; + this.addresses = addresses; + } + @Override + public Kind kind() + { + return Kind.STARTUP; + } + + @Override + public Result execute(ClusterMetadata prev) + { + ClusterMetadata.Transformer next = prev.transformer(); + if (!prev.directory.addresses.get(nodeId).equals(addresses)) + { + if (!prev.inProgressSequences.isEmpty()) + return new Rejected(INVALID, "Cannot update address of the node while there are in-progress sequences"); + + for (Map.Entry entry : prev.directory.addresses.entrySet()) + { + NodeAddresses existingAddresses = entry.getValue(); + NodeId existingNodeId = entry.getKey(); + if (!nodeId.equals(existingNodeId) && addresses.conflictsWith(existingAddresses)) + return new Rejected(INVALID, String.format("New addresses %s conflicts with existing node %s with addresses %s", addresses, entry.getKey(), existingAddresses)); + } + + next = next.withNewAddresses(nodeId, addresses); + Keyspaces allKeyspaces = prev.schema.getKeyspaces().withAddedOrReplaced(prev.schema.getKeyspaces()); + + DataPlacements newPlacement = ClusterMetadataService.instance() + .placementProvider() + .calculatePlacements(prev.nextEpoch(), + prev.tokenMap.toRanges(), + next.build().metadata, + allKeyspaces); + + next = next.with(newPlacement); + } + + if (!prev.directory.versions.get(nodeId).equals(nodeVersion)) + next = next.withVersion(nodeId, nodeVersion); + + return Transformation.success(next, LockedRanges.AffectedRanges.EMPTY); + } + + @Override + public String toString() + { + return "Startup{" + + "nodeId=" + nodeId + + ", nodeVersion=" + nodeVersion + + ", addresses=" + addresses + + '}'; + } + + @Override + public boolean allowDuringUpgrades() + { + return true; + } + + public static void maybeExecuteStartupTransformation(NodeId localNodeId) + { + Directory directory = ClusterMetadata.current().directory; + + if (!Objects.equals(directory.addresses.get(localNodeId), NodeAddresses.current()) || + !Objects.equals(directory.versions.get(localNodeId), NodeVersion.CURRENT)) + { + ClusterMetadataService.instance() + .commit(new Startup(localNodeId, NodeAddresses.current(), NodeVersion.CURRENT)); + } + } + + static class Serializer implements MetadataSerializer + { + @Override + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + Startup startup = (Startup)t; + NodeId.serializer.serialize(startup.nodeId, out, version); + NodeVersion.serializer.serialize(startup.nodeVersion, out, version); + NodeAddresses.serializer.serialize(startup.addresses, out, version); + } + + @Override + public Transformation deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + NodeVersion nodeVersion = NodeVersion.serializer.deserialize(in, version); + NodeAddresses addresses = NodeAddresses.serializer.deserialize(in, version); + return new Startup(nodeId, addresses, nodeVersion); + } + + @Override + public long serializedSize(Transformation t, Version version) + { + Startup startup = (Startup)t; + return NodeId.serializer.serializedSize(startup.nodeId, version) + + NodeVersion.serializer.serializedSize(startup.nodeVersion, version) + + NodeAddresses.serializer.serializedSize(startup.addresses, version); + } + } + +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/Unregister.java b/src/java/org/apache/cassandra/tcm/transformations/Unregister.java new file mode 100644 index 0000000000..ff1fc72621 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/Unregister.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Softwarea 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.tcm.transformations; + +import java.io.IOException; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; + +public class Unregister implements Transformation +{ + private static final Logger logger = LoggerFactory.getLogger(Unregister.class); + public static final Serializer serializer = new Serializer(); + + private final NodeId nodeId; + public Unregister(NodeId nodeId) + { + this.nodeId = nodeId; + } + + @Override + public Kind kind() + { + return Kind.UNREGISTER; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (!prev.directory.peerIds().contains(nodeId)) + return new Rejected(INVALID, String.format("Can not unregsiter %s since it is not present in the directory.", nodeId)); + + ClusterMetadata.Transformer next = prev.transformer() + .unregister(nodeId); + + return Transformation.success(next, LockedRanges.AffectedRanges.EMPTY); + } + + @VisibleForTesting + public static void unregister(NodeId nodeId) + { + ClusterMetadataService.instance() + .commit(new Unregister(nodeId)); + } + + public String toString() + { + return "Unregister{" + + ", nodeId=" + nodeId + + '}'; + } + + static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t instanceof Unregister; + Unregister register = (Unregister)t; + NodeId.serializer.serialize(register.nodeId, out, version); + } + + public Unregister deserialize(DataInputPlus in, Version version) throws IOException + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + return new Unregister(nodeId); + } + + public long serializedSize(Transformation t, Version version) + { + assert t instanceof Unregister; + Unregister unregister = (Unregister) t; + return NodeId.serializer.serializedSize(unregister.nodeId, version); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/UnsafeJoin.java b/src/java/org/apache/cassandra/tcm/transformations/UnsafeJoin.java new file mode 100644 index 0000000000..c6ccfbab27 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/UnsafeJoin.java @@ -0,0 +1,107 @@ +/* + * 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.tcm.transformations; + +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataKey; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.LockedRanges; + +public class UnsafeJoin extends PrepareJoin +{ + public static final Serializer serializer = new Serializer() + { + public UnsafeJoin construct(NodeId nodeId, Set tokens, PlacementProvider placementProvider, boolean joinTokenRing, boolean streamData) + { + assert joinTokenRing; + assert !streamData; + return new UnsafeJoin(nodeId, tokens, placementProvider); + } + }; + + public UnsafeJoin(NodeId nodeId, Set tokens, PlacementProvider placementProvider) + { + super(nodeId, tokens, placementProvider, true, false); + } + + @Override + public String toString() + { + return "UnsafeJoin{" + + "nodeId=" + nodeId + + ", tokens=" + tokens + + ", joinTokenRing=" + joinTokenRing + + ", streamData=" + streamData + + "}"; + } + + @Override + public Kind kind() + { + return Kind.UNSAFE_JOIN; + } + + @Override + public Result execute(ClusterMetadata prev) + { + Result result = super.execute(prev); + if (result.isRejected()) + return result; + + Success success = result.success(); + ClusterMetadata metadata = success.metadata; + Epoch forceEpoch = metadata.epoch; + metadata = success.metadata.forceEpoch(prev.epoch); + + BootstrapAndJoin plan = (BootstrapAndJoin) metadata.inProgressSequences.get(nodeId()); + + ImmutableSet.Builder modifiedKeys = ImmutableSet.builder(); + + success = plan.startJoin.execute(metadata).success(); + metadata = success.metadata.forceEpoch(prev.epoch); + modifiedKeys.addAll(success.affectedMetadata); + + success = plan.midJoin.execute(metadata).success(); + metadata = success.metadata.forceEpoch(prev.epoch); + modifiedKeys.addAll(success.affectedMetadata); + + success = plan.finishJoin.execute(metadata).success(); + metadata = success.metadata; + modifiedKeys.addAll(success.affectedMetadata); + + assert metadata.epoch.is(forceEpoch) : String.format("Epoch should have been %s but was %s", forceEpoch, metadata.epoch); + + return new Success(metadata, LockedRanges.AffectedRanges.EMPTY, modifiedKeys.build()); + } + + public static void unsafeJoin(NodeId nodeId, Set tokens) + { + UnsafeJoin join = new UnsafeJoin(nodeId, tokens, ClusterMetadataService.instance().placementProvider()); + ClusterMetadataService.instance().commit(join); + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/AdvanceCMSReconfiguration.java b/src/java/org/apache/cassandra/tcm/transformations/cms/AdvanceCMSReconfiguration.java new file mode 100644 index 0000000000..13f009f8e3 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/AdvanceCMSReconfiguration.java @@ -0,0 +1,393 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; +import static org.apache.cassandra.tcm.MultiStepOperation.Kind.RECONFIGURE_CMS; + +/** + * A step in a CMS Reconfiguration sequence. This may represent the addition of a new CMS member or the removal of an + * existing one. Member additions are actually further decomposed into a pair of distinct steps: the first adds the + * node as a passive member of the CMS that only receives committed log updates, while the second enables it to begin + * participating in reads and in quorums for commit. Each of these two steps will be implemented by an instance of this + * class. Removing a member is more straightforward and so is done in a single step. + * See the {@link #startAdd}, {@link #finishAdd} and {@link #executeRemove} emove} methods. + */ +public class AdvanceCMSReconfiguration implements Transformation +{ + public static final Serializer serializer = new Serializer(); + + // Identifies the position this instance represents in a sequence to reconfigure the CMS. Such sequences are dynamic + // and only contain a single element at any one time. Logically sequences of this specific type comprise multiple + // steps, which are created anew when we advance from step to step. + public final int sequenceIndex; + // Identifies the epoch enacted by the preceding step in this reconfiguration sequence. Used to construct a + // ProgressBarrier when stepping through the sequence. Initialising a completely new sequence is a special case here + // as there is no preceding epoch, so the factory method in ReconfigureCMS which does this will supply Epoch.EMPTY + // which results in ProgressBarrier.immediate() + public final Epoch latestModification; + public final LockedRanges.Key lockKey; + + public final PrepareCMSReconfiguration.Diff diff; + public final ReconfigureCMS.ActiveTransition activeTransition; + + public AdvanceCMSReconfiguration(int sequenceIndex, + Epoch latestModification, + LockedRanges.Key lockKey, + PrepareCMSReconfiguration.Diff diff, + ReconfigureCMS.ActiveTransition active) + { + this.sequenceIndex = sequenceIndex; + this.latestModification = latestModification; + this.lockKey = lockKey; + this.diff = diff; + this.activeTransition = active; + } + + @Override + public Kind kind() + { + return Kind.ADVANCE_CMS_RECONFIGURATION; + } + + @Override + public Result execute(ClusterMetadata prev) + { + InProgressSequences sequences = prev.inProgressSequences; + MultiStepOperation sequence = sequences.get(ReconfigureCMS.SequenceKey.instance); + + if (sequence == null) + return new Transformation.Rejected(INVALID, "Can't advance CMS Reconfiguration as it is not present in current metadata"); + + if (sequence.kind() != RECONFIGURE_CMS) + return new Transformation.Rejected(INVALID, "Can't advance CMS Reconfiguraton as in incompatible sequence was detected: " + sequence.kind()); + + ReconfigureCMS reconfigureCMS = (ReconfigureCMS) sequence; + if (reconfigureCMS.next.sequenceIndex != sequenceIndex) + return new Transformation.Rejected(INVALID, String.format("This transformation (%d) has already been applied. Expected: %d", sequenceIndex, reconfigureCMS.next.sequenceIndex)); + + // An active transition means that the preceding step in this sequences began adding a new member + if (activeTransition == null) + { + // Execute additions before removals to avoid shrinking the CMS to the extent that we cannot then expand it + if (!diff.additions.isEmpty()) + { + return startAdd(prev, reconfigureCMS); + } + // Any additions have already been completed, start removing the CMS members specified by the diff + else if (!diff.removals.isEmpty()) + { + return executeRemove(prev, reconfigureCMS); + } + // All additions and removals in the reconfiguration sequence have completed, the final step is to remove + // the sequence itselt from ClusterMetadata and release the lock + else + { + return Transformation.success(prev.transformer() + .with(prev.inProgressSequences.without(ReconfigureCMS.SequenceKey.instance)) + .with(prev.lockedRanges.unlock(lockKey)), + EntireRange.affectedRanges(prev)); + } + } + else + { + // A 2 step member addition is in progress, so complete it + return finishAdd(prev, reconfigureCMS, activeTransition.nodeId); + } + } + + /** + * Execute the transformation to begin adding a CMS member. + * Takes the node to be added from the diff and makes it a write replica of the CMS. + * Identifies the sources for streaming to it, which the reconfiguration sequence will initiate before attempting + * to execute the next step. + * Advances the sequence by constructing the next step and updating the stored sequences. + * @param prev + * @param sequence + * @return + * @throws Transformation.RejectedTransformationException + */ + private Transformation.Result startAdd(ClusterMetadata prev, ReconfigureCMS sequence) + { + // Pop the next node to be added from the list diff.additions + NodeId addition = diff.additions.get(0); + InetAddressAndPort endpoint = prev.directory.endpoint(addition); + Replica replica = new Replica(endpoint, entireRange, true); + List newAdditions = new ArrayList<>(diff.additions.subList(1, diff.additions.size())); + + // Check that the candidate is not already a CMS member + ReplicationParams metaParams = ReplicationParams.meta(prev); + RangesByEndpoint readReplicas = prev.placements.get(metaParams).reads.byEndpoint(); + RangesByEndpoint writeReplicas = prev.placements.get(metaParams).writes.byEndpoint(); + if (readReplicas.containsKey(endpoint) || writeReplicas.containsKey(endpoint)) + return new Transformation.Rejected(INVALID, "Endpoint is already a member of CMS"); + + + ClusterMetadata.Transformer transformer = prev.transformer(); + // Add the candidate as a write replica + DataPlacement.Builder builder = prev.placements.get(metaParams).unbuild() + .withWriteReplica(prev.nextEpoch(), replica); + transformer.with(prev.placements.unbuild().with(metaParams, builder.build()).build()); + + // Construct a set of sources for the new member to stream log tables from (essentially this is the existing members) + Set streamCandidates = new HashSet<>(); + for (Replica r : prev.placements.get(metaParams).reads.byEndpoint().flattenValues()) + { + if (!replica.equals(r)) + streamCandidates.add(r.endpoint()); + } + + // Set up the next step in the sequence. This encapsulates the entire state of the reconfiguration sequence, + // including the remaining add/remove operations and the streaming that needs to be done by the joining node + AdvanceCMSReconfiguration next = next(prev.nextEpoch(), + newAdditions, + diff.removals, + new ReconfigureCMS.ActiveTransition(addition, streamCandidates)); + // Create a new sequence instance with the next step to reflect that the state has progressed. + ReconfigureCMS advanced = sequence.advance(next); + // Finally, replace the existing reconfiguration sequence with this updated one. + transformer.with(prev.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, (ReconfigureCMS old) -> advanced)); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + /** + * Execute the transformation to finish adding a CMS member. + * Takes the node currently being added, which was obtained from the sequence's ActiveTransition and makes it a + * full (read/write) replica of the CMS. + * Advances the sequence by constructing the next step and updating the stored sequences. + * @param prev + * @param sequence + * @param addition + * @return + * @throws Transformation.RejectedTransformationException + */ + private Transformation.Result finishAdd(ClusterMetadata prev, ReconfigureCMS sequence, NodeId addition) + { + // Add the new member as a full read replica, able to participate in quorums for log updates + ReplicationParams metaParams = ReplicationParams.meta(prev); + InetAddressAndPort endpoint = prev.directory.endpoint(addition); + Replica replica = new Replica(endpoint, entireRange, true); + ClusterMetadata.Transformer transformer = prev.transformer(); + DataPlacement.Builder builder = prev.placements.get(metaParams) + .unbuild() + .withReadReplica(prev.nextEpoch(), replica); + transformer = transformer.with(prev.placements.unbuild().with(metaParams, builder.build()).build()); + + // Set up the next step in the sequence. This encapsulates the entire state of the reconfiguration sequence, + // which includes the remaining add/remove operations + AdvanceCMSReconfiguration next = next(prev.nextEpoch(), diff.additions, diff.removals, null); + // Create a new sequence instance with the next step to reflect that the state has progressed. + ReconfigureCMS advanced = sequence.advance(next); + // Finally, replace the existing reconfiguration sequence with this updated one. + transformer.with(prev.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, (ReconfigureCMS old) -> advanced)); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + /** + * Execute the transformation to remove a CMS member. + * Takes the node to be removed from the diff and removes it from the read/write replicas of the CMS. + * Advances the sequence by constructing the next step and updating the stored sequences. + */ + private Transformation.Result executeRemove(ClusterMetadata prev, ReconfigureCMS sequence) + { + // Pop the next member to be removed from the list diff.removals + NodeId removal = diff.removals.get(0); + List newRemovals = new ArrayList<>(diff.removals.subList(1, diff.removals.size())); + + // Check that the candidate is actually a CMS member + ClusterMetadata.Transformer transformer = prev.transformer(); + InetAddressAndPort endpoint = prev.directory.endpoint(removal); + Replica replica = new Replica(endpoint, entireRange, true); + ReplicationParams metaParams = ReplicationParams.meta(prev); + if (!prev.fullCMSMembers().contains(endpoint)) + return new Transformation.Rejected(INVALID, String.format("%s is not currently a CMS member, cannot remove it", endpoint)); + + // Check that the candidate is not the only CMS member + DataPlacement.Builder builder = prev.placements.get(metaParams).unbuild(); + builder.reads.withoutReplica(prev.nextEpoch(), replica); + builder.writes.withoutReplica(prev.nextEpoch(), replica); + DataPlacement proposed = builder.build(); + if (proposed.reads.byEndpoint().isEmpty() || proposed.writes.byEndpoint().isEmpty()) + return new Transformation.Rejected(INVALID, String.format("Removing %s will leave no nodes in CMS", endpoint)); + + // Actually remove the candidate + transformer = transformer.with(prev.placements.unbuild().with(metaParams, proposed).build()); + + // Set up the next step in the sequence. This encapsulates the entire state of the reconfiguration sequence, + // which includes the remaining add/remove operations + AdvanceCMSReconfiguration next = next(prev.nextEpoch(), diff.additions, newRemovals, null); + // Create a new sequence instance with the next step to reflect that the state has progressed. + ReconfigureCMS advanced = sequence.advance(next); + // Finally, replace the existing reconfiguration sequence with this updated one. + transformer.with(prev.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, (ReconfigureCMS old) -> advanced)); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + private AdvanceCMSReconfiguration next(Epoch latestModification, + List additions, + List removals, + ReconfigureCMS.ActiveTransition active) + { + return new AdvanceCMSReconfiguration(sequenceIndex + 1, + latestModification, + lockKey, + new PrepareCMSReconfiguration.Diff(additions, removals), + active); + } + + public boolean isLast() + { + if (!diff.additions.isEmpty()) + return false; + if (!diff.removals.isEmpty()) + return false; + if (activeTransition != null) + return false; + + return true; + } + + public String toString() + { + String current; + if (activeTransition == null) + { + if (!diff.additions.isEmpty()) + { + NodeId addition = diff.additions.get(0); + current = "StartAddToCMS(" + addition + ")"; + } + else if (!diff.removals.isEmpty()) + { + NodeId removal = diff.removals.get(0); + current = "RemoveFromCMS(" + removal + ")"; + } + else + { + current = "FinishReconfiguration()"; + } + } + else + { + current = "FinishCMSReconfiguration()"; + } + return "AdvanceCMSReconfiguration{" + + "idx=" + sequenceIndex + + ", current=" + current + + ", diff=" + diff + + ", activeTransition=" + activeTransition + + '}'; + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + AdvanceCMSReconfiguration transformation = (AdvanceCMSReconfiguration) t; + out.writeUnsignedVInt32(transformation.sequenceIndex); + Epoch.serializer.serialize(transformation.latestModification, out, version); + LockedRanges.Key.serializer.serialize(transformation.lockKey, out, version); + + PrepareCMSReconfiguration.Diff.serializer.serialize(transformation.diff, out, version); + + out.writeBoolean(transformation.activeTransition != null); + if (transformation.activeTransition != null) + { + ReconfigureCMS.ActiveTransition activeTransition = transformation.activeTransition; + NodeId.serializer.serialize(activeTransition.nodeId, out, version); + out.writeInt(activeTransition.streamCandidates.size()); + for (InetAddressAndPort e : activeTransition.streamCandidates) + InetAddressAndPort.MetadataSerializer.serializer.serialize(e, out, version); + } + } + + public AdvanceCMSReconfiguration deserialize(DataInputPlus in, Version version) throws IOException + { + int idx = in.readUnsignedVInt32(); + Epoch lastModified = Epoch.serializer.deserialize(in, version); + LockedRanges.Key lockKey = LockedRanges.Key.serializer.deserialize(in, version); + + PrepareCMSReconfiguration.Diff diff = PrepareCMSReconfiguration.Diff.serializer.deserialize(in, version); + + boolean hasActiveTransition = in.readBoolean(); + ReconfigureCMS.ActiveTransition activeTransition = null; + if (hasActiveTransition) + { + NodeId nodeId = NodeId.serializer.deserialize(in, version); + int streamCandidatesCount = in.readInt(); + Set streamCandidates = new HashSet<>(); + for (int i = 0; i < streamCandidatesCount; i++) + streamCandidates.add(InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version)); + activeTransition = new ReconfigureCMS.ActiveTransition(nodeId, streamCandidates); + } + + return new AdvanceCMSReconfiguration(idx, lastModified, lockKey, diff, activeTransition); + } + + public long serializedSize(Transformation t, Version version) + { + AdvanceCMSReconfiguration transformation = (AdvanceCMSReconfiguration) t; + long size = 0; + size += TypeSizes.sizeofUnsignedVInt(transformation.sequenceIndex); + size += Epoch.serializer.serializedSize(transformation.latestModification, version); + size += LockedRanges.Key.serializer.serializedSize(transformation.lockKey, version); + size += PrepareCMSReconfiguration.Diff.serializer.serializedSize(transformation.diff, version); + + size += TypeSizes.BOOL_SIZE; + if (transformation.activeTransition != null) + { + ReconfigureCMS.ActiveTransition activeTransition = transformation.activeTransition; + size += NodeId.serializer.serializedSize(activeTransition.nodeId, version); + size += TypeSizes.INT_SIZE; + for (InetAddressAndPort e : activeTransition.streamCandidates) + size += InetAddressAndPort.MetadataSerializer.serializer.serializedSize(e, version); + } + + return size; + } + } + +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/BaseMembershipTransformation.java b/src/java/org/apache/cassandra/tcm/transformations/cms/BaseMembershipTransformation.java new file mode 100644 index 0000000000..ae7a3042d4 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/BaseMembershipTransformation.java @@ -0,0 +1,95 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public abstract class BaseMembershipTransformation implements Transformation +{ + protected final InetAddressAndPort endpoint; + protected final Replica replica; + + protected BaseMembershipTransformation(InetAddressAndPort endpoint) + { + this.endpoint = endpoint; + this.replica = EntireRange.replica(endpoint); + } + + // TODO: to node id + public InetAddressAndPort getEndpoint() + { + return endpoint; + } + + public static abstract class SerializerBase implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + T transformation = (T) t; + InetAddressAndPort.MetadataSerializer.serializer.serialize(transformation.endpoint, out, version); + } + + public T deserialize(DataInputPlus in, Version version) throws IOException + { + InetAddressAndPort addr = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + return createTransformation(addr); + } + + public long serializedSize(Transformation t, Version version) + { + T transformation = (T) t; + return InetAddressAndPort.MetadataSerializer.serializer.serializedSize(transformation.endpoint, version); + } + + public abstract T createTransformation(InetAddressAndPort addr); + } + + public String toString() + { + return getClass().getSimpleName() + '{' + + "endpoint=" + endpoint + + ", replica=" + replica + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof BaseMembershipTransformation)) return false; + BaseMembershipTransformation that = (BaseMembershipTransformation) o; + return Objects.equals(endpoint, that.endpoint) && Objects.equals(replica, that.replica); + } + + @Override + public int hashCode() + { + return Objects.hash(kind(), endpoint, replica); + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/FinishAddToCMS.java b/src/java/org/apache/cassandra/tcm/transformations/cms/FinishAddToCMS.java new file mode 100644 index 0000000000..eacf236a92 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/FinishAddToCMS.java @@ -0,0 +1,111 @@ +/* + * 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.tcm.transformations.cms; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +/** + * This class along with AddToCMS, StartAddToCMS & RemoveFromCMS, contain a high degree of duplication with their intended + * replacements ReconfigureCMS and AdvanceCMSReconfiguration. This shouldn't be a big problem as the intention is to + * remove this superceded version asap. + * @deprecated in favour of ReconfigureCMS + */ +@Deprecated(since = "CEP-21") +public class FinishAddToCMS extends BaseMembershipTransformation +{ + public static final AsymmetricMetadataSerializer serializer = new SerializerBase() + { + public FinishAddToCMS createTransformation(InetAddressAndPort addr) + { + return new FinishAddToCMS(addr); + } + }; + + public FinishAddToCMS(InetAddressAndPort addr) + { + super(addr); + } + + @Override + public Kind kind() + { + return Kind.FINISH_ADD_TO_CMS; + } + + public Replica replicaForStreaming() + { + return replica; + } + + @Override + public Result execute(ClusterMetadata prev) + { + InProgressSequences sequences = prev.inProgressSequences; + NodeId targetNode = prev.directory.peerId(replica.endpoint()); + MultiStepOperation sequence = sequences.get(targetNode); + + if (sequence == null) + return new Rejected(INVALID, "Can't execute finish join as cluster metadata does not hold join sequence for this node"); + + if (!(sequence instanceof AddToCMS)) + return new Rejected(INVALID, "Can't execute finish join as cluster metadata contains a sequence of a different kind"); + + ReplicationParams metaParams = ReplicationParams.meta(prev); + InetAddressAndPort endpoint = prev.directory.endpoint(targetNode); + Replica replica = new Replica(endpoint, entireRange, true); + + ClusterMetadata.Transformer transformer = prev.transformer(); + DataPlacement.Builder builder = prev.placements.get(metaParams) + .unbuild() + .withReadReplica(prev.nextEpoch(), replica); + transformer = transformer.with(prev.placements.unbuild().with(metaParams, builder.build()).build()) + .with(prev.inProgressSequences.without(targetNode)); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + public String toString() + { + return "FinishAddMember{" + + "endpoint=" + endpoint + + ", replica=" + replica + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return super.equals(o); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/Initialize.java b/src/java/org/apache/cassandra/tcm/transformations/cms/Initialize.java new file mode 100644 index 0000000000..07dbcc116a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/Initialize.java @@ -0,0 +1,92 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; + +import org.apache.cassandra.auth.AuthKeyspace; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.SystemDistributedKeyspace; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; +import org.apache.cassandra.tracing.TraceKeyspace; + +public class Initialize extends ForceSnapshot +{ + public static final AsymmetricMetadataSerializer serializer = new AsymmetricMetadataSerializer() + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + Initialize initialize = (Initialize) t; + ClusterMetadata.serializer.serialize(initialize.baseState, out, version); + } + + public Initialize deserialize(DataInputPlus in, Version version) throws IOException + { + return new Initialize(ClusterMetadata.serializer.deserialize(in, version)); + } + + public long serializedSize(Transformation t, Version version) + { + Initialize initialize = (Initialize) t; + return ClusterMetadata.serializer.serializedSize(initialize.baseState, version); + } + }; + + public Initialize(ClusterMetadata baseState) + { + super(baseState); + } + + public Kind kind() + { + return Kind.INITIALIZE_CMS; + } + + public Result execute(ClusterMetadata prev) + { + ClusterMetadata next = baseState; + DistributedSchema initialSchema = new DistributedSchema(setUpDistributedSystemKeyspaces(next)); + ClusterMetadata.Transformer transformer = next.transformer().with(initialSchema); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + public Keyspaces setUpDistributedSystemKeyspaces(ClusterMetadata next) + { + Keyspaces keyspaces = next.schema.getKeyspaces(); + return keyspaces.withAddedOrReplaced(Keyspaces.of(TraceKeyspace.metadata(), + SystemDistributedKeyspace.metadata(), + AuthKeyspace.metadata())); + } + + @Override + public String toString() + { + return "Initialize{" + + "baseState = " + baseState.epoch + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/PreInitialize.java b/src/java/org/apache/cassandra/tcm/transformations/cms/PreInitialize.java new file mode 100644 index 0000000000..ef6f8adb3a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/PreInitialize.java @@ -0,0 +1,135 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.LockedRanges; + +public class PreInitialize implements Transformation +{ + public static Serializer serializer = new Serializer(); + + public final InetAddressAndPort addr; + + private PreInitialize(InetAddressAndPort addr) + { + this.addr = addr; + } + + public static PreInitialize forTesting() + { + return new PreInitialize(null); + } + + public static PreInitialize blank() + { + return new PreInitialize(null); + } + + public static PreInitialize withFirstCMS(InetAddressAndPort addr) + { + return new PreInitialize(addr); + } + + public Kind kind() + { + return Kind.PRE_INITIALIZE_CMS; + } + + public Result execute(ClusterMetadata metadata) + { + assert metadata.epoch.isBefore(Epoch.FIRST); + assert metadata.period == Period.EMPTY; + + ClusterMetadata.Transformer transformer = metadata.transformer(false); + if (addr != null) + { + DataPlacement.Builder dataPlacementBuilder = DataPlacement.builder(); + Replica replica = new Replica(addr, + DatabaseDescriptor.getPartitioner().getMinimumToken(), + DatabaseDescriptor.getPartitioner().getMinimumToken(), + true); + dataPlacementBuilder.reads.withReplica(metadata.nextEpoch(), replica); + dataPlacementBuilder.writes.withReplica(metadata.nextEpoch(), replica); + DataPlacements initialPlacement = metadata.placements.unbuild().with(ReplicationParams.meta(metadata), dataPlacementBuilder.build()).build(); + + transformer.with(initialPlacement); + } + ClusterMetadata.Transformer.Transformed transformed = transformer.build(); + metadata = transformed.metadata; + assert metadata.epoch.is(Epoch.FIRST) : metadata.epoch; + assert metadata.period == Period.FIRST : metadata.period; + + return new Success(metadata, LockedRanges.AffectedRanges.EMPTY, transformed.modifiedKeys); + } + + public String toString() + { + return "PreInitialize{" + + "addr=" + addr + + '}'; + } + + static class Serializer implements AsymmetricMetadataSerializer + { + + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + assert t.kind() == Kind.PRE_INITIALIZE_CMS; + PreInitialize bcms = (PreInitialize)t; + out.writeBoolean(bcms.addr != null); + if (bcms.addr != null) + InetAddressAndPort.MetadataSerializer.serializer.serialize(((PreInitialize)t).addr, out, version); + } + + public PreInitialize deserialize(DataInputPlus in, Version version) throws IOException + { + boolean hasAddr = in.readBoolean(); + if (!hasAddr) + return PreInitialize.blank(); + + InetAddressAndPort addr = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + return new PreInitialize(addr); + } + + public long serializedSize(Transformation t, Version version) + { + PreInitialize bcms = (PreInitialize)t; + long size = TypeSizes.sizeof(bcms.addr != null); + + return size + (bcms.addr != null ? InetAddressAndPort.MetadataSerializer.serializer.serializedSize(((PreInitialize)t).addr, version) : 0); + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/PrepareCMSReconfiguration.java b/src/java/org/apache/cassandra/tcm/transformations/cms/PrepareCMSReconfiguration.java new file mode 100644 index 0000000000..2480f37f65 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/PrepareCMSReconfiguration.java @@ -0,0 +1,289 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.CMSPlacementStrategy; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.MetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +public class PrepareCMSReconfiguration +{ + private static Transformation.Result executeInternal(ClusterMetadata prev, Function transform, Diff diff) + { + LockedRanges.Key lockKey = LockedRanges.keyFor(prev.nextEpoch()); + Set cms = prev.fullCMSMembers().stream().map(prev.directory::peerId).collect(Collectors.toSet()); + Set tmp = new HashSet<>(cms); + tmp.addAll(diff.additions); + tmp.removeAll(diff.removals); + if (tmp.isEmpty()) + return new Transformation.Rejected(INVALID, String.format("Applying diff %s to %s would leave CMS empty", cms, diff)); + + ClusterMetadata.Transformer transformer = prev.transformer() + .with(prev.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, + ReconfigureCMS.newSequence(lockKey, diff))) + .with(prev.lockedRanges.lock(lockKey, LockedRanges.AffectedRanges.singleton(ReplicationParams.meta(prev), entireRange))); + return Transformation.success(transform.apply(transformer), LockedRanges.AffectedRanges.EMPTY); + } + + public static class Simple implements Transformation + { + public static final Simple.Serializer serializer = new Serializer(); + + private final NodeId toReplace; + + public Simple(NodeId toReplace) + { + this.toReplace = toReplace; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_SIMPLE_CMS_RECONFIGURATION; + } + + @Override + public Result execute(ClusterMetadata prev) + { + if (!prev.fullCMSMembers().contains(prev.directory.getNodeAddresses(toReplace).broadcastAddress)) + return new Rejected(INVALID, String.format("%s is not a member of CMS. Members: %s", toReplace, prev.fullCMSMembers())); + + ReplicationParams metaParams = ReplicationParams.meta(prev); + CMSPlacementStrategy placementStrategy = CMSPlacementStrategy.fromReplicationParams(metaParams, nodeId -> !nodeId.equals(toReplace)); + Set currentCms = prev.fullCMSMembers() + .stream() + .map(prev.directory::peerId) + .collect(Collectors.toSet()); + + Set withoutReplaced = new HashSet<>(currentCms); + withoutReplaced.remove(toReplace); + Set newCms = placementStrategy.reconfigure(withoutReplaced, prev); + Diff diff = diff(currentCms, newCms); + return executeInternal(prev, t -> t, diff); + } + + public String toString() + { + return "PrepareCMSReconfiguration#Simple{" + + "toReplace=" + toReplace + + '}'; + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + Simple tranformation = (Simple) t; + NodeId.serializer.serialize(tranformation.toReplace, out, version); + } + + public Simple deserialize(DataInputPlus in, Version version) throws IOException + { + return new Simple(NodeId.serializer.deserialize(in, version)); + } + + public long serializedSize(Transformation t, Version version) + { + Simple tranformation = (Simple) t; + return NodeId.serializer.serializedSize(tranformation.toReplace, version); + } + } + } + + public static class Complex implements Transformation + { + public static final Complex.Serializer serializer = new Complex.Serializer(); + + private final ReplicationParams replicationParams; + + public Complex(ReplicationParams replicationParams) + { + this.replicationParams = replicationParams; + } + + @Override + public Kind kind() + { + return Kind.PREPARE_COMPLEX_CMS_RECONFIGURATION; + } + + @Override + public Result execute(ClusterMetadata prev) + { + KeyspaceMetadata keyspace = prev.schema.getKeyspaceMetadata(SchemaConstants.METADATA_KEYSPACE_NAME); + KeyspaceMetadata newKeyspace = keyspace.withSwapped(new KeyspaceParams(keyspace.params.durableWrites, replicationParams)); + + CMSPlacementStrategy placementStrategy = CMSPlacementStrategy.fromReplicationParams(replicationParams, nodeId -> true); + + Set currentCms = prev.fullCMSMembers() + .stream() + .map(prev.directory::peerId) + .collect(Collectors.toSet()); + + Set newCms = placementStrategy.reconfigure(currentCms, prev); + Diff diff = diff(currentCms, newCms); + + return executeInternal(prev, + transformer -> transformer.with(prev.placements.replaceParams(prev.nextEpoch(),ReplicationParams.meta(prev), replicationParams)) + .with(new DistributedSchema(prev.schema.getKeyspaces().withAddedOrUpdated(newKeyspace))), + diff); + } + + public String toString() + { + return "PrepareCMSReconfiguration#Complex{" + + "replicationParams=" + replicationParams + + '}'; + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + Complex tranformation = (Complex) t; + ReplicationParams.serializer.serialize(tranformation.replicationParams, out, version); + } + + public Complex deserialize(DataInputPlus in, Version version) throws IOException + { + return new Complex(ReplicationParams.serializer.deserialize(in, version)); + } + + public long serializedSize(Transformation t, Version version) + { + Complex tranformation = (Complex) t; + return ReplicationParams.serializer.serializedSize(tranformation.replicationParams, version); + } + } + } + + public static Diff diff(Set currentCms, Set newCms) + { + List additions = new ArrayList<>(); + for (NodeId node : newCms) + { + if (!currentCms.contains(node)) + additions.add(node); + } + + List removals = new ArrayList<>(); + for (NodeId nodeId : currentCms) + { + if (!newCms.contains(nodeId)) + removals.add(nodeId); + } + + return new Diff(additions, removals); + } + + public static class Diff + { + public static final Serializer serializer = new Serializer(); + + public final List additions; + public final List removals; + + public Diff(List additions, List removals) + { + this.additions = additions; + this.removals = removals; + } + + public String toString() + { + return "Diff{" + + "additions=" + additions + + ", removals=" + removals + + '}'; + } + + public static class Serializer implements MetadataSerializer + { + public void serialize(Diff diff, DataOutputPlus out, Version version) throws IOException + { + out.writeInt(diff.additions.size()); + for (NodeId addition : diff.additions) + NodeId.serializer.serialize(addition, out, version); + + out.writeInt(diff.removals.size()); + for (NodeId removal : diff.removals) + NodeId.serializer.serialize(removal, out, version); + } + + public Diff deserialize(DataInputPlus in, Version version) throws IOException + { + int additionsCount = in.readInt(); + List additions = new ArrayList<>(); + for (int i = 0; i < additionsCount; i++) + { + NodeId addition = NodeId.serializer.deserialize(in, version); + additions.add(addition); + } + + int removalsCount = in.readInt(); + List removals = new ArrayList<>(); + for (int i = 0; i < removalsCount; i++) + { + NodeId removal = NodeId.serializer.deserialize(in, version); + removals.add(removal); + } + + return new Diff(additions, removals); + } + + public long serializedSize(Diff diff, Version version) + { + long size = 0; + size += TypeSizes.INT_SIZE; + for (NodeId addition : diff.additions) + size += NodeId.serializer.serializedSize(addition, version); + size += TypeSizes.INT_SIZE; + for (NodeId removal : diff.removals) + size += NodeId.serializer.serializedSize(removal, version); + + return size; + } + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/RemoveFromCMS.java b/src/java/org/apache/cassandra/tcm/transformations/cms/RemoveFromCMS.java new file mode 100644 index 0000000000..d60616ab70 --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/RemoveFromCMS.java @@ -0,0 +1,175 @@ +/* + * 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.tcm.transformations.cms; + +import java.io.IOException; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +/** + * This class along with AddToCMS, StartAddToCMS & FinishAddToCMS, contain a high degree of duplication with their intended + * replacements ReconfigureCMS and AdvanceCMSReconfiguration. This shouldn't be a big problem as the intention is to + * remove this superceded version asap. + * @deprecated in favour of ReconfigureCMS + */ +@Deprecated(since = "CEP-21") +public class RemoveFromCMS extends BaseMembershipTransformation +{ + private static final Logger logger = LoggerFactory.getLogger(RemoveFromCMS.class); + public static final Serializer serializer = new Serializer(); + // Note: use CMS reconfiguration rather than manual addition/removal + public static final int MIN_SAFE_CMS_SIZE = 3; + public final boolean force; + + public RemoveFromCMS(InetAddressAndPort addr, boolean force) + { + super(addr); + this.force = force; + } + + @Override + public Kind kind() + { + return Kind.REMOVE_FROM_CMS; + } + + @Override + public Result execute(ClusterMetadata prev) + { + InProgressSequences sequences = prev.inProgressSequences; + if (sequences.get(ReconfigureCMS.SequenceKey.instance) != null) + return new Rejected(INVALID, String.format("Cannot remove %s from CMS as a CMS reconfiguration is currently active", endpoint)); + + if (!prev.fullCMSMembers().contains(endpoint)) + return new Transformation.Rejected(INVALID, String.format("%s is not currently a CMS member, cannot remove it", endpoint)); + + NodeId nodeId = prev.directory.peerId(endpoint); + MultiStepOperation sequence = sequences.get(nodeId); + // This is theoretically permissible, but feels unsafe + if (sequence != null) + return new Transformation.Rejected(INVALID, String.format("Can't remove %s from CMS as there are ongoing range movements on it", endpoint)); + + ReplicationParams metaParams = ReplicationParams.meta(prev); + DataPlacement placements = prev.placements.get(metaParams); + + int minProposedSize = (int) Math.min(placements.reads.forRange(replica.range()).get().stream().filter(r -> !r.endpoint().equals(endpoint)).count(), + placements.writes.forRange(replica.range()).get().stream().filter(r -> !r.endpoint().equals(endpoint)).count()); + if (minProposedSize < MIN_SAFE_CMS_SIZE) + { + logger.warn("Removing {} from CMS members would reduce the service size to {} which is below the " + + "configured safe quorum {}. This requires the force option which is set to {}, {}proceeding", + endpoint, minProposedSize, MIN_SAFE_CMS_SIZE, force, force ? "" : "not "); + if (!force) + { + return new Transformation.Rejected(INVALID, String.format("Removing %s from the CMS would reduce the number of members to " + + "%d, below the configured soft minimum %d. " + + "To perform this operation anyway, resubmit with force=true", + endpoint, minProposedSize, MIN_SAFE_CMS_SIZE)); + } + } + + if (minProposedSize == 0) + return new Transformation.Rejected(INVALID, String.format("Removing %s from the CMS would leave no members in CMS.", endpoint)); + + ClusterMetadata.Transformer transformer = prev.transformer(); + Replica replica = new Replica(endpoint, entireRange, true); + + DataPlacement.Builder builder = prev.placements.get(metaParams).unbuild(); + builder.reads.withoutReplica(prev.nextEpoch(), replica); + builder.writes.withoutReplica(prev.nextEpoch(), replica); + DataPlacement proposed = builder.build(); + + if (proposed.reads.byEndpoint().isEmpty() || proposed.writes.byEndpoint().isEmpty()) + return new Transformation.Rejected(INVALID, String.format("Removing %s will leave no nodes in CMS", endpoint)); + + return Transformation.success(transformer.with(prev.placements.unbuild().with(metaParams, proposed).build()), + EntireRange.affectedRanges(prev)); + } + + @Override + public String toString() + { + return getClass().getSimpleName() + '{' + + "endpoint=" + endpoint + + ", replica=" + replica + + ", force=" + force + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof RemoveFromCMS)) return false; + RemoveFromCMS that = (RemoveFromCMS) o; + return Objects.equals(endpoint, that.endpoint) && Objects.equals(replica, that.replica) && force == that.force; + } + + @Override + public int hashCode() + { + return Objects.hash(kind(), endpoint, replica, force); + } + + public static class Serializer implements AsymmetricMetadataSerializer + { + public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException + { + RemoveFromCMS remove = (RemoveFromCMS)t; + InetAddressAndPort.MetadataSerializer.serializer.serialize(remove.endpoint, out, version); + out.writeBoolean(remove.force); + } + + public RemoveFromCMS deserialize(DataInputPlus in, Version version) throws IOException + { + InetAddressAndPort addr = InetAddressAndPort.MetadataSerializer.serializer.deserialize(in, version); + boolean force = in.readBoolean(); + return new RemoveFromCMS(addr, force); + } + + public long serializedSize(Transformation t, Version version) + { + RemoveFromCMS remove = (RemoveFromCMS)t; + return InetAddressAndPort.MetadataSerializer.serializer.serializedSize(remove.endpoint, version) + + TypeSizes.sizeof(remove.force); + } + } +} diff --git a/src/java/org/apache/cassandra/tcm/transformations/cms/StartAddToCMS.java b/src/java/org/apache/cassandra/tcm/transformations/cms/StartAddToCMS.java new file mode 100644 index 0000000000..af4f4f8e2a --- /dev/null +++ b/src/java/org/apache/cassandra/tcm/transformations/cms/StartAddToCMS.java @@ -0,0 +1,121 @@ +/* + * 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.tcm.transformations.cms; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer; + +import static org.apache.cassandra.exceptions.ExceptionCode.INVALID; +import static org.apache.cassandra.tcm.ownership.EntireRange.entireRange; + +/** + * This class along with AddToCMS, FinishAddToCMS & RemoveFromCMS, contain a high degree of duplication with their intended + * replacements ReconfigureCMS and AdvanceCMSReconfiguration. This shouldn't be a big problem as the intention is to + * remove this superceded version asap. + * @deprecated in favour of ReconfigureCMS + */ +@Deprecated(since = "CEP-21") +public class StartAddToCMS extends BaseMembershipTransformation +{ + public static final AsymmetricMetadataSerializer serializer = new SerializerBase() + { + public StartAddToCMS createTransformation(InetAddressAndPort addr) + { + return new StartAddToCMS(addr); + } + }; + + public StartAddToCMS(InetAddressAndPort addr) + { + super(addr); + } + + @Override + public Kind kind() + { + return Kind.START_ADD_TO_CMS; + } + + @Override + public Result execute(ClusterMetadata prev) + { + NodeId nodeId = prev.directory.peerId(endpoint); + MultiStepOperation sequence = prev.inProgressSequences.get(nodeId); + if (sequence != null) + return new Rejected(INVALID, String.format("Cannot add node to CMS, since it already has an active in-progress sequence %s", sequence)); + if (prev.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance) != null) + return new Rejected(INVALID, String.format("Cannot add node to CMS as a CMS reconfiguration is currently active")); + + Replica replica = new Replica(endpoint, entireRange, true); + ReplicationParams metaParams = ReplicationParams.meta(prev); + RangesByEndpoint readReplicas = prev.placements.get(metaParams).reads.byEndpoint(); + RangesByEndpoint writeReplicas = prev.placements.get(metaParams).writes.byEndpoint(); + + if (readReplicas.containsKey(endpoint) || writeReplicas.containsKey(endpoint)) + return new Transformation.Rejected(INVALID, "Endpoint is already a member of CMS"); + + ClusterMetadata.Transformer transformer = prev.transformer(); + DataPlacement.Builder builder = prev.placements.get(metaParams).unbuild() + .withWriteReplica(prev.nextEpoch(), replica); + + transformer.with(prev.placements.unbuild().with(metaParams, builder.build()).build()); + + Set streamCandidates = new HashSet<>(); + for (Replica r : prev.placements.get(metaParams).reads.byEndpoint().flattenValues()) + { + if (!replica.equals(r)) + streamCandidates.add(r.endpoint()); + } + + AddToCMS joinSequence = new AddToCMS(prev.nextEpoch(), nodeId, streamCandidates, new FinishAddToCMS(endpoint)); + transformer = transformer.with(prev.inProgressSequences.with(nodeId, joinSequence)); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + @Override + public String toString() + { + return "StartAddToCMS{" + + "endpoint=" + endpoint + + ", replica=" + replica + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return super.equals(o); + } +} diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index ab38720c94..e6ada7fae4 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -102,6 +102,7 @@ import org.apache.cassandra.net.MessagingServiceMBean; import org.apache.cassandra.service.ActiveRepairServiceMBean; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.CacheServiceMBean; +import org.apache.cassandra.tcm.CMSOperationsMBean; import org.apache.cassandra.service.GCInspector; import org.apache.cassandra.service.GCInspectorMXBean; import org.apache.cassandra.service.StorageProxy; @@ -120,6 +121,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Uninterruptibles; + +import org.apache.cassandra.tcm.CMSOperations; import org.apache.cassandra.tools.nodetool.GetTimeout; import org.apache.cassandra.utils.NativeLibrary; @@ -147,6 +150,7 @@ public class NodeProbe implements AutoCloseable protected MBeanServerConnection mbeanServerConn; protected CompactionManagerMBean compactionProxy; protected StorageServiceMBean ssProxy; + protected CMSOperationsMBean cmsProxy; protected GossiperMBean gossProxy; protected MemoryMXBean memProxy; protected GCInspectorMXBean gcProxy; @@ -259,6 +263,8 @@ public class NodeProbe implements AutoCloseable { ObjectName name = new ObjectName(ssObjName); ssProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class); + name = new ObjectName(CMSOperations.MBEAN_OBJECT_NAME); + cmsProxy = JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class); name = new ObjectName(MessagingService.MBEAN_NAME); msProxy = JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class); name = new ObjectName(StreamManagerMBean.OBJECT_NAME); @@ -1006,7 +1012,12 @@ public class NodeProbe implements AutoCloseable public void removeNode(String token) { - ssProxy.removeNode(token); + removeNode(token, false); + } + + public void removeNode(String token, boolean force) + { + ssProxy.removeNode(token, force); } public String getRemovalStatus(boolean withPort) @@ -1021,7 +1032,7 @@ public class NodeProbe implements AutoCloseable public void assassinateEndpoint(String address) throws UnknownHostException { - gossProxy.assassinateEndpoint(address); + ssProxy.assassinateEndpoint(address); } public List reloadSeeds() @@ -1215,10 +1226,16 @@ public class NodeProbe implements AutoCloseable return spProxy; } - public StorageServiceMBean getStorageService() { + public StorageServiceMBean getStorageService() + { return ssProxy; } + public CMSOperationsMBean getCMSOperationsProxy() + { + return cmsProxy; + } + public GossiperMBean getGossProxy() { return gossProxy; @@ -2346,6 +2363,11 @@ public class NodeProbe implements AutoCloseable table.printTo(out); } + + public void abortBootstrap(String nodeId, String endpoint) + { + ssProxy.abortBootstrap(nodeId, endpoint); + } } class ColumnFamilyStoreMBeanIterator implements Iterator> diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index a630e66deb..8e2c6f45cd 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -94,6 +94,7 @@ public class NodeTool public int execute(String... args) { List> commands = newArrayList( + AbortBootstrap.class, Assassinate.class, CassHelp.class, CIDRFilteringStats.class, @@ -106,6 +107,7 @@ public class NodeTool DataPaths.class, Decommission.class, DescribeCluster.class, + DescribeCMS.class, DescribeRing.class, DisableAuditLog.class, DisableAutoCompaction.class, @@ -156,6 +158,7 @@ public class NodeTool GossipInfo.class, Import.class, Info.class, + InitializeCMS.class, InvalidateCIDRPermissionsCache.class, InvalidateCounterCache.class, InvalidateCredentialsCache.class, @@ -179,6 +182,7 @@ public class NodeTool Rebuild.class, RebuildIndex.class, RecompressSSTables.class, + ReconfigureCMS.class, Refresh.class, RefreshSizeEstimates.class, ReloadLocalSchema.class, @@ -194,6 +198,7 @@ public class NodeTool ResumeHandoff.class, Ring.class, Scrub.class, + SealPeriod.class, SetAuthCacheConfig.class, SetBatchlogReplayThrottle.class, SetCacheCapacity.class, diff --git a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java index 74204fcdee..c1029fd755 100644 --- a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java +++ b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java @@ -35,6 +35,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; @@ -58,10 +59,9 @@ public class SSTableExpiredBlockers } Util.initDatabaseDescriptor(); - + ClusterMetadataService.initializeForTools(false); String keyspace = args[args.length - 2]; String columnfamily = args[args.length - 1]; - Schema.instance.loadFromDisk(); TableMetadata metadata = Schema.instance.validateTable(keyspace, columnfamily); diff --git a/src/java/org/apache/cassandra/tools/SSTableExport.java b/src/java/org/apache/cassandra/tools/SSTableExport.java index 4eaec602f1..05ec576553 100644 --- a/src/java/org/apache/cassandra/tools/SSTableExport.java +++ b/src/java/org/apache/cassandra/tools/SSTableExport.java @@ -48,6 +48,7 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; + import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST; diff --git a/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java b/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java index 0aa9e10814..6010777018 100644 --- a/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java +++ b/src/java/org/apache/cassandra/tools/SSTableLevelResetter.java @@ -31,6 +31,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.StatsComponent; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.JVMStabilityInspector; /** @@ -60,18 +61,15 @@ public class SSTableLevelResetter } Util.initDatabaseDescriptor(); - + ClusterMetadataService.initializeForTools(false); // TODO several daemon threads will run from here. // So we have to explicitly call System.exit. try { - // load keyspace descriptions. - Schema.instance.loadFromDisk(); - String keyspaceName = args[1]; String columnfamily = args[2]; // validate columnfamily - if (Schema.instance.getTableMetadataRef(keyspaceName, columnfamily) == null) + if (Schema.instance.getTableMetadata(keyspaceName, columnfamily) == null) { System.err.println("ColumnFamily not found: " + keyspaceName + "/" + columnfamily); System.exit(1); diff --git a/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java b/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java index 0c3d3e75ea..e0aa8bd9e0 100644 --- a/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java +++ b/src/java/org/apache/cassandra/tools/SSTableOfflineRelevel.java @@ -44,6 +44,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; /** * Create a decent leveling for the given keyspace/column family @@ -88,13 +89,12 @@ public class SSTableOfflineRelevel } Util.initDatabaseDescriptor(); - + ClusterMetadataService.initializeForTools(false); boolean dryRun = args[0].equals("--dry-run"); String keyspace = args[args.length - 2]; String columnfamily = args[args.length - 1]; - Schema.instance.loadFromDisk(); - if (Schema.instance.getTableMetadataRef(keyspace, columnfamily) == null) + if (Schema.instance.getTableMetadata(keyspace, columnfamily) == null) throw new IllegalArgumentException(String.format("Unknown keyspace/table %s.%s", keyspace, columnfamily)); diff --git a/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java b/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java index 4d2acd20d4..1dd0ba53d6 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java +++ b/src/java/org/apache/cassandra/tools/StandaloneSSTableUtil.java @@ -18,10 +18,11 @@ */ package org.apache.cassandra.tools; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.OutputHandler; import org.apache.commons.cli.*; @@ -43,13 +44,13 @@ public class StandaloneSSTableUtil public static void main(String args[]) { + Options options = Options.parseArgs(args); try { // load keyspace descriptions. Util.initDatabaseDescriptor(); - Schema.instance.loadFromDisk(); - + ClusterMetadataService.initializeForTools(false); TableMetadata metadata = Schema.instance.getTableMetadata(options.keyspaceName, options.cfName); if (metadata == null) throw new IllegalArgumentException(String.format("Unknown keyspace/table %s.%s", diff --git a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java index 36fda102e9..e7afc255c0 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneScrubber.java +++ b/src/java/org/apache/cassandra/tools/StandaloneScrubber.java @@ -50,6 +50,7 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tools.BulkLoader.CmdLineOptions; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.OutputHandler; @@ -87,12 +88,10 @@ public class StandaloneScrubber DatabaseDescriptor.toolInitialization(false); //Necessary for testing else Util.initDatabaseDescriptor(); + ClusterMetadataService.initializeForTools(false); try { - // load keyspace descriptions. - Schema.instance.loadFromDisk(); - if (Schema.instance.getKeyspaceMetadata(options.keyspaceName) == null) throw new IllegalArgumentException(String.format("Unknown keyspace %s", options.keyspaceName)); diff --git a/src/java/org/apache/cassandra/tools/StandaloneSplitter.java b/src/java/org/apache/cassandra/tools/StandaloneSplitter.java index f9f248a7b8..db95190415 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneSplitter.java +++ b/src/java/org/apache/cassandra/tools/StandaloneSplitter.java @@ -46,7 +46,7 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTable; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; -import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.JVMStabilityInspector; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST; @@ -73,9 +73,7 @@ public class StandaloneSplitter try { - // load keyspace descriptions. - Schema.instance.loadFromDisk(); - + ClusterMetadataService.initializeForTools(false); String ksName = null; String cfName = null; Map> parsedFilenames = new HashMap>(); diff --git a/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java b/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java index 1009c3fc41..52e9f8c295 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java +++ b/src/java/org/apache/cassandra/tools/StandaloneUpgrader.java @@ -41,6 +41,7 @@ import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.OutputHandler; @@ -61,13 +62,10 @@ public class StandaloneUpgrader DatabaseDescriptor.toolInitialization(false); //Necessary for testing else Util.initDatabaseDescriptor(); - + ClusterMetadataService.initializeForTools(false); try { - // load keyspace descriptions. - Schema.instance.loadFromDisk(); - - if (Schema.instance.getTableMetadataRef(options.keyspace, options.cf) == null) + if (Schema.instance.getTableMetadata(options.keyspace, options.cf) == null) throw new IllegalArgumentException(String.format("Unknown keyspace/table %s.%s", options.keyspace, options.cf)); diff --git a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java index 547a1e05f2..241fe2d432 100644 --- a/src/java/org/apache/cassandra/tools/StandaloneVerifier.java +++ b/src/java/org/apache/cassandra/tools/StandaloneVerifier.java @@ -47,6 +47,7 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.Throwables; @@ -77,19 +78,16 @@ public class StandaloneVerifier System.exit(1); } initDatabaseDescriptorForTool(); - + ClusterMetadataService.initializeForTools(false); System.out.println("sstableverify using the following options: " + options); List sstables = new ArrayList<>(); int exitCode = 0; try { - // load keyspace descriptions. - Schema.instance.loadFromDisk(); - boolean hasFailed = false; - if (Schema.instance.getTableMetadataRef(options.keyspaceName, options.cfName) == null) + if (Schema.instance.getTableMetadata(options.keyspaceName, options.cfName) == null) throw new IllegalArgumentException(String.format("Unknown keyspace/table %s.%s", options.keyspaceName, options.cfName)); diff --git a/src/java/org/apache/cassandra/tools/TransformClusterMetadataHelper.java b/src/java/org/apache/cassandra/tools/TransformClusterMetadataHelper.java new file mode 100644 index 0000000000..cc97d6bfff --- /dev/null +++ b/src/java/org/apache/cassandra/tools/TransformClusterMetadataHelper.java @@ -0,0 +1,95 @@ +/* + * 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.tools; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.io.util.FileInputStreamPlus; +import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.serialization.Version; + +public class TransformClusterMetadataHelper +{ + public static void main(String ... args) throws IOException + { + if (args.length < 2) + { + System.err.println("Usage: addtocmstool []"); + System.exit(1); + } + String sourceFile = args[0]; + Version serializationVersion = NodeVersion.CURRENT.serializationVersion(); + if (args.length > 2) + serializationVersion = Version.valueOf(args[2]); + + // Make sure the partitioner we use to manipulate the metadata is the same one used to generate it + IPartitioner partitioner = null; + try (FileInputStreamPlus fisp = new FileInputStreamPlus(sourceFile)) + { + // skip over the prefix specifying the metadata version + fisp.readUnsignedVInt32(); + partitioner = ClusterMetadata.Serializer.getPartitioner(fisp, serializationVersion); + } + DatabaseDescriptor.toolInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(partitioner); + ClusterMetadataService.initializeForTools(false); + ClusterMetadata metadata = ClusterMetadataService.deserializeClusterMetadata(sourceFile); + System.out.println("Old CMS: " + metadata.placements.get(ReplicationParams.meta(metadata))); + metadata = makeCMS(metadata, InetAddressAndPort.getByNameUnchecked(args[1])); + System.out.println("New CMS: " + metadata.placements.get(ReplicationParams.meta(metadata))); + Path p = Files.createTempFile("clustermetadata", "dump"); + try (FileOutputStreamPlus out = new FileOutputStreamPlus(p)) + { + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, metadata, out, serializationVersion); + } + System.out.println(p.toString()); + } + + public static ClusterMetadata makeCMS(ClusterMetadata metadata, InetAddressAndPort endpoint) + { + ReplicationParams metaParams = ReplicationParams.meta(metadata); + Iterable currentReplicas = metadata.placements.get(metaParams).writes.byEndpoint().flattenValues(); + DataPlacement.Builder builder = metadata.placements.get(metaParams).unbuild(); + for (Replica replica : currentReplicas) + { + builder.withoutReadReplica(metadata.epoch, replica) + .withoutWriteReplica(metadata.epoch, replica); + } + Replica newCMS = EntireRange.replica(endpoint); + builder.withReadReplica(metadata.epoch, newCMS) + .withWriteReplica(metadata.epoch, newCMS); + return metadata.transformer().with(metadata.placements.unbuild().with(metaParams, + builder.build()) + .build()) + .build().metadata; + } +} diff --git a/src/java/org/apache/cassandra/tools/Util.java b/src/java/org/apache/cassandra/tools/Util.java index d8ef121f89..4c4557624f 100644 --- a/src/java/org/apache/cassandra/tools/Util.java +++ b/src/java/org/apache/cassandra/tools/Util.java @@ -319,7 +319,7 @@ public final class Util IPartitioner partitioner = FBUtilities.newPartitioner(desc); - TableMetadata.Builder builder = TableMetadata.builder("keyspace", "table").partitioner(partitioner); + TableMetadata.Builder builder = TableMetadata.builder("keyspace", "table").partitioner(partitioner).offline(); header.getStaticColumns().entrySet().stream() .forEach(entry -> { ColumnIdentifier ident = ColumnIdentifier.getInterned(UTF8Type.instance.getString(entry.getKey()), true); diff --git a/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java b/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java new file mode 100644 index 0000000000..4c180ad81c --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java @@ -0,0 +1,48 @@ +/* + * 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.tools.nodetool; + + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool.NodeToolCmd; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +@Command(name = "abortbootstrap", description = "Abort a failed bootstrap") +public class AbortBootstrap extends NodeToolCmd +{ + @Option(title = "node id", name = "--node", description = "Node ID of the node that failed bootstrap", required = false) + private String nodeId = EMPTY; + + @Option(title = "ip", name = "--ip", description = "IP of the node that failed bootstrap", required = false) + private String endpoint = EMPTY; + + + @Override + public void execute(NodeProbe probe) + { + if (isEmpty(nodeId) && isEmpty(endpoint)) + throw new IllegalArgumentException("Either --node or --ip needs to be set"); + if (!isEmpty(nodeId) && !isEmpty(endpoint)) + throw new IllegalArgumentException("Only one of --node or --ip need to be set"); + probe.abortBootstrap(nodeId, endpoint); + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/DescribeCMS.java b/src/java/org/apache/cassandra/tools/nodetool/DescribeCMS.java new file mode 100644 index 0000000000..813a2d01d5 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/DescribeCMS.java @@ -0,0 +1,44 @@ +/* + * 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.tools.nodetool; + +import java.util.Map; + +import io.airlift.airline.Command; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; + +@Command(name = "describecms", description = "Describe the current Cluster Metadata Service") +public class DescribeCMS extends NodeTool.NodeToolCmd +{ + @Override + protected void execute(NodeProbe probe) + { + Map info = probe.getCMSOperationsProxy().describeCMS(); + System.out.printf("Cluster Metadata Service:%n"); + System.out.printf("Members: %s%n", info.get("MEMBERS")); + System.out.printf("Is Member: %s%n", info.get("IS_MEMBER")); + System.out.printf("Service State: %s%n", info.get("SERVICE_STATE")); + System.out.printf("Is Migrating: %s%n", info.get("IS_MIGRATING")); + System.out.printf("Epoch: %s%n", info.get("EPOCH")); + System.out.printf("Local Pending Count: %s%n", info.get("LOCAL_PENDING")); + System.out.printf("Commits Paused: %s%n", info.get("COMMITS_PAUSED")); + System.out.printf("Replication factor: %s%n", info.get("REPLICATION_FACTOR")); + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWithAll.java b/src/java/org/apache/cassandra/tools/nodetool/InitializeCMS.java similarity index 53% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWithAll.java rename to src/java/org/apache/cassandra/tools/nodetool/InitializeCMS.java index 4db8e2b6d9..316862aeec 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWithAll.java +++ b/src/java/org/apache/cassandra/tools/nodetool/InitializeCMS.java @@ -16,23 +16,24 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tools.nodetool; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.simulator.Actions.ReliableAction; +import java.util.ArrayList; +import java.util.List; -import static org.apache.cassandra.simulator.Action.Modifiers.NONE; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; -class OnInstanceGossipWithAll extends ReliableAction +@Command(name = "initializecms", description = "Upgrade from gossip and initialize CMS") +public class InitializeCMS extends NodeTool.NodeToolCmd { - OnInstanceGossipWithAll(ClusterActions actions, int from) + @Option(title = "ignored endpoints", name = {"-i", "--ignore"}, description = "Hosts to ignore due to them being down", required = false) + private List endpoint = new ArrayList<>(); + @Override + protected void execute(NodeProbe probe) { - super("Send Shutdown from " + from + " to all", NONE, RELIABLE_NO_TIMEOUTS, - () -> actions.gossipWithAll(from)); + probe.getCMSOperationsProxy().initializeCMS(endpoint); } - OnInstanceGossipWithAll(ClusterActions actions, IInvokableInstance from) - { - this(actions, from.config().num()); - } -} +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/tools/nodetool/Join.java b/src/java/org/apache/cassandra/tools/nodetool/Join.java index c77559cd1a..74f9ad96f5 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Join.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Join.java @@ -32,8 +32,6 @@ public class Join extends NodeToolCmd public void execute(NodeProbe probe) { checkState(!probe.isJoined(), "This node has already joined the ring."); - checkState(!probe.isBootstrapMode(), "Cannot join the ring until bootstrap completes"); - try { probe.joinRing(); diff --git a/src/java/org/apache/cassandra/tools/nodetool/ReconfigureCMS.java b/src/java/org/apache/cassandra/tools/nodetool/ReconfigureCMS.java new file mode 100644 index 0000000000..0feff7d9ea --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/ReconfigureCMS.java @@ -0,0 +1,122 @@ +/* + * 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.tools.nodetool; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.airlift.airline.Arguments; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; + +@Command(name = "reconfigurecms", description = "Reconfigure replication factor of CMS") +public class ReconfigureCMS extends NodeTool.NodeToolCmd +{ + @Option(title = "status", + name = {"--status"}, + description = "Poll status of the reconfigurecms command. All other flags and arguments are ignored when this one is used.") + private boolean status = false; + + @Option(title = "resume", + name = {"-r", "--resume"}, + description = "Whether or not a previously interrupted sequence should be resumed") + private boolean resume = false; + + @Option(title = "sync", + name = {"-s", "--sync"}, + description = "Whether or not nodetool should block and wait for reconfiguration to finish") + private boolean sync = false; + + @Arguments(usage = "[] or : ... ", description = "Replication factor of new CMS") + private List args = new ArrayList<>(); + + @Override + protected void execute(NodeProbe probe) + { + if (status) + { + Map> status = probe.getCMSOperationsProxy().reconfigureCMSStatus(); + if (status == null) + { + System.out.println("No active reconfiguration"); + } + else + { + for (Map.Entry> e : status.entrySet()) + System.out.println(String.format("%s: %s", e.getKey(), e.getValue())); + } + return; + } + if (resume) + { + if (!args.isEmpty()) + throw new IllegalArgumentException("Replication factor should not be set if previous operation is resumed"); + + probe.getCMSOperationsProxy().resumeReconfigureCms(); + return; + } + + if (args.isEmpty()) + throw new IllegalArgumentException("Replication factor is empty"); + + Map parsedRfs = new HashMap<>(args.size()); + for (String rf : args) + { + if (!rf.contains(":")) + { + if (args.size() > 1) + throw new IllegalArgumentException("Simple placement can only specify a single replication factor accross all data centers"); + int parsedRf; + try + { + parsedRf = Integer.parseInt(args.get(0)); + } + catch (Throwable t) + { + throw new IllegalArgumentException(String.format("Can not parse replication factor from %s", args.get(0))); + } + probe.getCMSOperationsProxy().reconfigureCMS(parsedRf, sync); + return; + } + else + { + String[] splits = rf.split(":"); + if (splits.length > 2) + throw new IllegalArgumentException(String.format("Can not parse replication factor %s", rf)); + String dc = splits[0]; + int parsedRf; + try + { + parsedRf = Integer.parseInt(splits[1]); + } + catch (Throwable t) + { + throw new IllegalArgumentException(String.format("Can not parse replication factor from %s", args.get(0))); + } + parsedRfs.put(dc, parsedRf); + } + } + + probe.getCMSOperationsProxy().reconfigureCMS(parsedRfs, sync); + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java b/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java index e26cce36ee..89912ec640 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java +++ b/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java @@ -17,33 +17,38 @@ */ package org.apache.cassandra.tools.nodetool; -import static org.apache.commons.lang3.StringUtils.EMPTY; +import java.util.List; + import io.airlift.airline.Arguments; import io.airlift.airline.Command; import org.apache.cassandra.tools.NodeProbe; import org.apache.cassandra.tools.NodeTool.NodeToolCmd; -@Command(name = "removenode", description = "Show status of current node removal, force completion of pending removal or remove provided ID") +@Command(name = "removenode", description = "Show status of current node removal, abort removal or remove provided ID") public class RemoveNode extends NodeToolCmd { - @Arguments(title = "remove_operation", usage = "||", description = "Show status of current node removal, force completion of pending removal, or remove provided ID", required = true) - private String removeOperation = EMPTY; + @Arguments(title = "remove_operation", usage = "| || --force", description = "Show status of current node removal, abort removal, or remove provided ID", required = true) + private List removeOperation = null; @Override public void execute(NodeProbe probe) { - switch (removeOperation) + switch (removeOperation.get(0)) { case "status": probe.output().out.println("RemovalStatus: " + probe.getRemovalStatus(printPort)); break; case "force": - probe.output().out.println("RemovalStatus: " + probe.getRemovalStatus(printPort)); - probe.forceRemoveCompletion(); + throw new IllegalArgumentException("Can't force a nodetool removenode. Instead abort the ongoing removenode and retry."); + case "abort": + if (removeOperation.size() < 2) + probe.output().err.print("Abort requires the node id to abort the removal for."); + probe.getCMSOperationsProxy().cancelInProgressSequences(removeOperation.get(1), "REMOVE"); break; default: - probe.removeNode(removeOperation); + boolean force = removeOperation.size() > 1 && removeOperation.get(1).equals("--force"); + probe.removeNode(removeOperation.get(0), force); break; } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapping.java b/src/java/org/apache/cassandra/tools/nodetool/SealPeriod.java similarity index 66% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapping.java rename to src/java/org/apache/cassandra/tools/nodetool/SealPeriod.java index c8a6f5ddb2..e7db33f453 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSetBootstrapping.java +++ b/src/java/org/apache/cassandra/tools/nodetool/SealPeriod.java @@ -16,14 +16,18 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.cluster; +package org.apache.cassandra.tools.nodetool; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingBootstrappingRunner; +import io.airlift.airline.Command; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool.NodeToolCmd; -class OnInstanceSetBootstrapping extends ClusterReliableAction +@Command(name = "sealperiod", description = "seal current period in the metadata log") +public class SealPeriod extends NodeToolCmd { - OnInstanceSetBootstrapping(ClusterActions actions, int on) + @Override + public void execute(NodeProbe probe) { - super("Set " + on + " to Bootstrapping", actions, on, addToRingBootstrappingRunner(actions.cluster.get(on))); + probe.getCMSOperationsProxy().sealPeriod(); } } diff --git a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java index 3fe32b824e..12288e9e73 100644 --- a/src/java/org/apache/cassandra/tracing/TraceKeyspace.java +++ b/src/java/org/apache/cassandra/tracing/TraceKeyspace.java @@ -29,7 +29,6 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaConstants; @@ -47,7 +46,7 @@ public final class TraceKeyspace { } - private static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.getInt(); + public static final int DEFAULT_RF = CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.getInt(); /** * Generation is used as a timestamp for automatic table creation on startup. @@ -70,33 +69,31 @@ public final class TraceKeyspace public static final String EVENTS = "events"; public static final Set TABLE_NAMES = ImmutableSet.of(SESSIONS, EVENTS); + public static final String SESSIONS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "session_id uuid," + + "command text," + + "client inet," + + "coordinator inet," + + "coordinator_port int," + + "duration int," + + "parameters map," + + "request text," + + "started_at timestamp," + + "PRIMARY KEY ((session_id)))"; private static final TableMetadata Sessions = - parse(SESSIONS, - "tracing sessions", - "CREATE TABLE %s (" - + "session_id uuid," - + "command text," - + "client inet," - + "coordinator inet," - + "coordinator_port int," - + "duration int," - + "parameters map," - + "request text," - + "started_at timestamp," - + "PRIMARY KEY ((session_id)))"); + parse(SESSIONS, "tracing sessions", SESSIONS_CQL); + public static final String EVENTS_CQL = "CREATE TABLE IF NOT EXISTS %s (" + + "session_id uuid," + + "event_id timeuuid," + + "activity text," + + "source inet," + + "source_port int," + + "source_elapsed int," + + "thread text," + + "PRIMARY KEY ((session_id), event_id))"; private static final TableMetadata Events = - parse(EVENTS, - "tracing events", - "CREATE TABLE %s (" - + "session_id uuid," - + "event_id timeuuid," - + "activity text," - + "source inet," - + "source_port int," - + "source_elapsed int," - + "thread text," - + "PRIMARY KEY ((session_id), event_id))"); + parse(EVENTS, "tracing events", EVENTS_CQL); private static TableMetadata parse(String table, String description, String cql) { @@ -124,10 +121,9 @@ public final class TraceKeyspace Row.SimpleBuilder rb = builder.row(); rb.ttl(ttl) .add("client", client) - .add("coordinator", FBUtilities.getBroadcastAddressAndPort().getAddress()); - if (!Gossiper.instance.hasMajorVersion3Nodes()) - rb.add("coordinator_port", FBUtilities.getBroadcastAddressAndPort().getPort()); - rb.add("request", request) + .add("coordinator", FBUtilities.getBroadcastAddressAndPort().getAddress()) + .add("coordinator_port", FBUtilities.getBroadcastAddressAndPort().getPort()) + .add("request", request) .add("started_at", new Date(startedAt)) .add("command", command) .appendAll("parameters", parameters); @@ -151,10 +147,9 @@ public final class TraceKeyspace .ttl(ttl); rowBuilder.add("activity", message) - .add("source", FBUtilities.getBroadcastAddressAndPort().getAddress()); - if (!Gossiper.instance.hasMajorVersion3Nodes()) - rowBuilder.add("source_port", FBUtilities.getBroadcastAddressAndPort().getPort()); - rowBuilder.add("thread", threadName); + .add("source", FBUtilities.getBroadcastAddressAndPort().getAddress()) + .add("source_port", FBUtilities.getBroadcastAddressAndPort().getPort()) + .add("thread", threadName); if (elapsed >= 0) rowBuilder.add("source_elapsed", elapsed); diff --git a/src/java/org/apache/cassandra/utils/BiMultiValMap.java b/src/java/org/apache/cassandra/utils/BiMultiValMap.java index 7a87217967..f439c5c496 100644 --- a/src/java/org/apache/cassandra/utils/BiMultiValMap.java +++ b/src/java/org/apache/cassandra/utils/BiMultiValMap.java @@ -20,6 +20,7 @@ package org.apache.cassandra.utils; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import com.google.common.collect.HashMultimap; @@ -143,4 +144,19 @@ public class BiMultiValMap implements Map { return reverseMap.keySet(); } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof BiMultiValMap)) return false; + BiMultiValMap that = (BiMultiValMap) o; + return forwardMap.equals(that.forwardMap) && reverseMap.equals(that.reverseMap); + } + + @Override + public int hashCode() + { + return Objects.hash(forwardMap, reverseMap); + } } diff --git a/src/java/org/apache/cassandra/utils/CassandraVersion.java b/src/java/org/apache/cassandra/utils/CassandraVersion.java index 766f0e8173..f4c3dd7388 100644 --- a/src/java/org/apache/cassandra/utils/CassandraVersion.java +++ b/src/java/org/apache/cassandra/utils/CassandraVersion.java @@ -50,6 +50,7 @@ public class CassandraVersion implements Comparable private static final Pattern PATTERN = Pattern.compile(VERSION_REGEXP); + public static final CassandraVersion CASSANDRA_5_0 = new CassandraVersion("5.0").familyLowerBound.get(); public static final CassandraVersion CASSANDRA_4_1 = new CassandraVersion("4.1").familyLowerBound.get(); public static final CassandraVersion CASSANDRA_4_0 = new CassandraVersion("4.0").familyLowerBound.get(); public static final CassandraVersion CASSANDRA_4_0_RC2 = new CassandraVersion(4, 0, 0, NO_HOTFIX, new String[] {"rc2"}, null); diff --git a/src/java/org/apache/cassandra/utils/CounterId.java b/src/java/org/apache/cassandra/utils/CounterId.java index fc0385ec9a..1de8b5477b 100644 --- a/src/java/org/apache/cassandra/utils/CounterId.java +++ b/src/java/org/apache/cassandra/utils/CounterId.java @@ -20,7 +20,7 @@ package org.apache.cassandra.utils; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicReference; -import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.tcm.ClusterMetadata; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUIDAsBytes; @@ -138,7 +138,7 @@ public class CounterId implements Comparable LocalCounterIdHolder() { - current = new AtomicReference<>(wrap(ByteBufferUtil.bytes(SystemKeyspace.getOrInitializeLocalHostId()))); + current = new AtomicReference<>(wrap(ByteBufferUtil.bytes(ClusterMetadata.current().myNodeId().toUUID()))); } CounterId get() diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java index fe31ecd3a6..b66b8df6a3 100644 --- a/src/java/org/apache/cassandra/utils/FBUtilities.java +++ b/src/java/org/apache/cassandra/utils/FBUtilities.java @@ -34,6 +34,7 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -60,6 +61,7 @@ import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import com.google.common.base.Preconditions; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -544,6 +546,7 @@ public class FBUtilities } catch (ExecutionException ee) { + logger.info("Exception occurred in async code", ee); throw Throwables.cleaned(ee); } catch (InterruptedException ie) @@ -552,6 +555,28 @@ public class FBUtilities } } + public static T waitOnFuture(Future future, Duration timeout) + { + Preconditions.checkArgument(!timeout.isNegative(), "Timeout must not be negative, provided %s", timeout); + try + { + return future.get(timeout.toNanos(), TimeUnit.NANOSECONDS); + } + catch (ExecutionException ee) + { + logger.info("Exception occurred in async code", ee); + throw Throwables.cleaned(ee); + } + catch (InterruptedException ie) + { + throw new AssertionError(ie); + } + catch (TimeoutException e) + { + throw new RuntimeException("Timeout - task did not finish in " + timeout); + } + } + public static > F waitOnFirstFuture(Iterable futures) { return waitOnFirstFuture(futures, 100); @@ -1068,9 +1093,9 @@ public class FBUtilities sb.append(str).append(lineSep); while ((str = err.readLine()) != null) sb.append(str).append(lineSep); - throw new IOException("Exception while executing the command: "+ StringUtils.join(pb.command(), " ") + + throw new IOException("Exception while executing the command: " + StringUtils.join(pb.command(), " ") + ", command error Code: " + errCode + - ", command output: "+ sb.toString()); + ", command output: " + sb); } } } diff --git a/src/java/org/apache/cassandra/utils/NativeLibrary.java b/src/java/org/apache/cassandra/utils/NativeLibrary.java index 9348433939..0f7172b5ae 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibrary.java +++ b/src/java/org/apache/cassandra/utils/NativeLibrary.java @@ -433,4 +433,9 @@ public final class NativeLibrary return -1; } + + public static boolean isEnabled() + { + return REQUIRE; + } } diff --git a/src/java/org/apache/cassandra/utils/RecomputingSupplier.java b/src/java/org/apache/cassandra/utils/RecomputingSupplier.java deleted file mode 100644 index 055443923b..0000000000 --- a/src/java/org/apache/cassandra/utils/RecomputingSupplier.java +++ /dev/null @@ -1,125 +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.utils; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - -import org.apache.cassandra.utils.concurrent.AsyncPromise; -import org.apache.cassandra.utils.concurrent.Future; -import org.apache.cassandra.utils.concurrent.Promise; - -/** - * Supplier that caches the last computed value until it is reset, forcing every caller of - * {@link RecomputingSupplier#get(long, TimeUnit)} to wait until this value is computed if - * it was not computed yet. - * - * Calling {@link RecomputingSupplier#recompute()} won't reset value for the already - * waiting consumers, but instead will schedule one recomputation as soon as current one is done. - */ -public class RecomputingSupplier -{ - private final Supplier supplier; - private final AtomicReference> cached = new AtomicReference<>(null); - private final AtomicBoolean workInProgress = new AtomicBoolean(false); - private final ExecutorService executor; - - public RecomputingSupplier(Supplier supplier, ExecutorService executor) - { - this.supplier = supplier; - this.executor = executor; - } - - public void recompute() - { - Future current = cached.get(); - boolean origWip = workInProgress.get(); - - if (origWip || (current != null && !current.isDone())) - { - if (cached.get() != current) - executor.submit(this::recompute); - return; // if work is has not started yet, schedule task for the future - } - - assert current == null || current.isDone(); - - // The work is not in progress, and current future is done. Try to submit a new task. - Promise lazyValue = new AsyncPromise<>(); - if (cached.compareAndSet(current, lazyValue)) - executor.submit(() -> doWork(lazyValue)); - else - executor.submit(this::recompute); // Lost CAS, resubmit - } - - private void doWork(Promise lazyValue) - { - T value = null; - Throwable err = null; - try - { - sanityCheck(workInProgress.compareAndSet(false, true)); - value = supplier.get(); - } - catch (Throwable t) - { - err = t; - } - finally - { - sanityCheck(workInProgress.compareAndSet(true, false)); - } - - if (err == null) - lazyValue.trySuccess(value); - else - lazyValue.tryFailure(err); - } - - private static void sanityCheck(boolean check) - { - assert check : "At most one task should be executing using this executor"; - } - - public T get(long timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException - { - Future lazyValue = cached.get(); - - // recompute was never called yet, return null. - if (lazyValue == null) - return null; - - return lazyValue.get(timeout, timeUnit); - } - - public String toString() - { - return "RecomputingSupplier{" + - "supplier=" + supplier + - ", cached=" + cached + - ", workInProgress=" + workInProgress + - ", executor=" + executor + - '}'; - } -} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/utils/btree/AbstractBTreeMap.java b/src/java/org/apache/cassandra/utils/btree/AbstractBTreeMap.java new file mode 100644 index 0000000000..0db33a52cc --- /dev/null +++ b/src/java/org/apache/cassandra/utils/btree/AbstractBTreeMap.java @@ -0,0 +1,157 @@ +/* + * 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.utils.btree; + +import java.util.AbstractMap; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +public abstract class AbstractBTreeMap extends AbstractMap +{ + protected final Object[] tree; + protected final KeyComparator comparator; + + protected AbstractBTreeMap(Object[] tree, KeyComparator comparator) + { + this.tree = tree; + this.comparator = comparator; + } + + /** + * return a new map containing provided key and value + * + * @throws IllegalStateException if the key already exists in the map + */ + public abstract AbstractBTreeMap with(K key, V value); + + /** + * returns a new map containing provided key and value - replaces existing key + */ + public abstract AbstractBTreeMap withForce(K key, V value); + + /** + * returns a new map without key + */ + public abstract AbstractBTreeMap without(K key); + + @Override + public int size() + { + return BTree.size(tree); + } + + @Override + public boolean isEmpty() + { + return BTree.isEmpty(tree); + } + + @Override + public boolean containsKey(Object key) + { + return get(key) != null; + } + + @Override + public boolean containsValue(Object value) + { + Iterator> iter = BTree.iterator(tree); + while (iter.hasNext()) + { + Entry entry = iter.next(); + if (entry.getValue().equals(value)) + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + public V get(Object key) + { + if (key == null) + throw new NullPointerException(); + Entry entry = BTree.find(tree, comparator, new Entry<>((K)key, null)); + if (entry != null) + return entry.getValue(); + return null; + } + + private Set keySet = null; + @Override + public Set keySet() + { + if (keySet == null) + keySet = BTreeSet.wrap(BTree.transformAndFilter(tree, (entry) -> ((Map.Entry)entry).getKey()), comparator.keyComparator); + return keySet; + } + + @Override + public Set values() + { + ImmutableSet.Builder b = ImmutableSet.builder(); + for (Map.Entry e : entrySet()) + b.add(e.getValue()); + return b.build(); + } + + @Override + public Set> entrySet() + { + return BTreeSet.wrap(tree, comparator); + } + + public V put(K key, V value) { throw new UnsupportedOperationException(); } + public V forcePut(K ignoredKey, V ignoredVal) { throw new UnsupportedOperationException(); } + public V remove(Object key) { throw new UnsupportedOperationException();} + public void putAll(Map m) { throw new UnsupportedOperationException(); } + public void clear() { throw new UnsupportedOperationException(); } + public Map.Entry pollFirstEntry() { throw new UnsupportedOperationException(); } + public Map.Entry pollLastEntry() { throw new UnsupportedOperationException(); } + + protected static class KeyComparator implements Comparator> + { + protected final Comparator keyComparator; + + protected KeyComparator(Comparator keyComparator) + { + this.keyComparator = keyComparator; + } + + @Override + public int compare(Map.Entry o1, Map.Entry o2) + { + return keyComparator.compare(o1.getKey(), o2.getKey()); + } + } + + static class Entry extends AbstractMap.SimpleEntry + { + public Entry(K key, V value) + { + super(key, value); + } + + public V setValue(V value) { throw new UnsupportedOperationException(); } + + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeBiMap.java b/src/java/org/apache/cassandra/utils/btree/BTreeBiMap.java new file mode 100644 index 0000000000..1e1984e635 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/btree/BTreeBiMap.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils.btree; + +import java.util.Comparator; +import java.util.Set; + +import com.google.common.collect.BiMap; + +import static java.util.Comparator.naturalOrder; + +public class BTreeBiMap extends AbstractBTreeMap implements BiMap +{ + private final Object[] inverse; + private final KeyComparator valueComparator; + + protected static BTreeBiMap withComparators(Object[] tree, Object [] inverse, Comparator comparator, Comparator valueComparator) + { + return new BTreeBiMap<>(tree, inverse, new KeyComparator<>(comparator), new KeyComparator<>(valueComparator)); + } + + private BTreeBiMap(Object[] tree, Object [] inverse, KeyComparator comparator, KeyComparator valueComparator) + { + super(tree, comparator); + this.valueComparator = valueComparator; + this.inverse = inverse; + } + + public static BTreeBiMap empty(Comparator comparator, Comparator valueComparator) + { + return withComparators(BTree.empty(), BTree.empty(), comparator, valueComparator); + } + + public static , V extends Comparable> BTreeBiMap empty() + { + return BTreeBiMap.empty(naturalOrder(), naturalOrder()); + } + + @Override + public BiMap inverse() + { + return new BTreeBiMap<>(inverse, tree, valueComparator, comparator); + } + + @Override + public BTreeBiMap with(K key, V value) + { + if (key == null || value == null) + throw new NullPointerException(); + AbstractBTreeMap.Entry entry = new AbstractBTreeMap.Entry<>(key, value); + AbstractBTreeMap.Entry inverseEntry = new AbstractBTreeMap.Entry<>(value, key); + if (BTree.find(tree, comparator, entry) != null) + throw new IllegalArgumentException("Key already exists in map: " + key); + if (BTree.find(inverse, valueComparator, inverseEntry) != null) + throw new IllegalArgumentException("Value already exists in map: " + value); + + return new BTreeBiMap<>(BTree.update(tree, new Object[]{ entry }, comparator, UpdateFunction.noOp()), + BTree.update(inverse, new Object[] { new AbstractBTreeMap.Entry<>(value, key) }, valueComparator, UpdateFunction.noOp()), + comparator, + valueComparator); + } + + @Override + public BTreeBiMap withForce(K key, V value) + { + // todo: optimise + return without(key).with(key, value); + } + + public BTreeBiMap without(K key) + { + AbstractBTreeMap.Entry entry = new AbstractBTreeMap.Entry<>(key, null); + AbstractBTreeMap.Entry existingEntry = BTree.find(tree, comparator, entry); + if (existingEntry == null) + return this; + + Object[] newTree = BTreeRemoval.remove(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)); + Object[] newInverse = BTreeRemoval.remove(inverse, valueComparator, new AbstractBTreeMap.Entry<>(existingEntry.getValue(), null)); + return new BTreeBiMap<>(newTree, newInverse, comparator, valueComparator); + } + + public Set values() + { + return inverse().keySet(); + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeMap.java b/src/java/org/apache/cassandra/utils/btree/BTreeMap.java new file mode 100644 index 0000000000..2d8e92a298 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/btree/BTreeMap.java @@ -0,0 +1,232 @@ +/* + * 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.utils.btree; + +import java.util.Comparator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.SortedMap; + +import org.apache.cassandra.utils.BulkIterator; + +import static java.util.Comparator.naturalOrder; + + +public class BTreeMap extends AbstractBTreeMap implements NavigableMap +{ + protected static BTreeMap withComparator(Object[] tree, Comparator comparator) + { + return new BTreeMap<>(tree, new KeyComparator<>(comparator)); + } + + protected BTreeMap(Object[] tree, KeyComparator comparator) + { + super(tree, comparator); + } + + public static BTreeMap empty(Comparator comparator) + { + return withComparator(BTree.empty(), comparator); + } + + public static , V> BTreeMap empty() + { + return BTreeMap.empty(naturalOrder()); + } + + @Override + public BTreeMap with(K key, V value) + { + if (key == null || value == null) + throw new NullPointerException(); + + AbstractBTreeMap.Entry entry = new AbstractBTreeMap.Entry<>(key, value); + AbstractBTreeMap.Entry existing; + if ((existing = BTree.find(tree, comparator, entry)) != null && !existing.equals(entry)) + throw new IllegalStateException("Map already contains " + key); + return new BTreeMap<>(BTree.update(tree, new Object[]{ entry }, comparator, UpdateFunction.noOp()), comparator); + } + + public BTreeMap withForce(K key, V value) + { + if (key == null || value == null) + throw new NullPointerException(); + AbstractBTreeMap.Entry entry = new AbstractBTreeMap.Entry<>(key, value); + return new BTreeMap<>(BTree.update(tree, new Object[] { entry }, comparator, UpdateFunction.Simple.of((a, b) -> b)), comparator); + } + + public BTreeMap without(K key) + { + if (key == null) + throw new NullPointerException(); + + return new BTreeMap<>(BTreeRemoval.remove(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)), comparator); + } + + @Override + public Map.Entry lowerEntry(K key) + { + return BTree.lower(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)); + } + + @Override + public K lowerKey(K key) + { + Map.Entry entry = lowerEntry(key); + return entry == null ? null : entry.getKey(); + } + + @Override + public Map.Entry floorEntry(K key) + { + return BTree.floor(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)); + } + + @Override + public K floorKey(K key) + { + Map.Entry entry = floorEntry(key); + return entry == null ? null : entry.getKey(); + } + + @Override + public Map.Entry ceilingEntry(K key) + { + return BTree.ceil(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)); + } + + @Override + public K ceilingKey(K key) + { + Map.Entry entry = ceilingEntry(key); + return entry == null ? null : entry.getKey(); + } + + @Override + public Map.Entry higherEntry(K key) + { + return BTree.higher(tree, comparator, new AbstractBTreeMap.Entry<>(key, null)); + } + + @Override + public K higherKey(K key) + { + Map.Entry entry = higherEntry(key); + return entry == null ? null : entry.getKey(); + } + + @Override + @SuppressWarnings("unchecked") + public Map.Entry firstEntry() + { + if (isEmpty()) + return null; + return (AbstractBTreeMap.Entry) BTree.iterator(tree).next(); + } + + @Override + @SuppressWarnings("unchecked") + public Map.Entry lastEntry() + { + return getEntry(size() - 1); + } + + @Override + public NavigableMap descendingMap() + { + return new BTreeMap<>(BTree.build(BulkIterator.of(BTree.iterable(tree, BTree.Dir.DESC).iterator()), BTree.size(tree), UpdateFunction.noOp), + new KeyComparator<>(comparator.keyComparator.reversed())); + } + + @Override + public NavigableSet navigableKeySet() + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public NavigableSet descendingKeySet() + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public NavigableMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public NavigableMap headMap(K toKey, boolean inclusive) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public NavigableMap tailMap(K fromKey, boolean inclusive) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public Comparator comparator() + { + return comparator.keyComparator; + } + + @Override + public SortedMap subMap(K fromKey, K toKey) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public SortedMap headMap(K toKey) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public SortedMap tailMap(K fromKey) + { + throw new UnsupportedOperationException("todo"); + } + + @Override + public K firstKey() + { + if (BTree.isEmpty(tree)) + return null; + return BTree.>findByIndex(tree, 0).getKey(); + } + + @Override + public K lastKey() + { + if (BTree.isEmpty(tree)) + return null; + return getEntry(size() - 1).getKey(); + } + + private Map.Entry getEntry(int idx) + { + return BTree.findByIndex(tree, idx); + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeMultimap.java b/src/java/org/apache/cassandra/utils/btree/BTreeMultimap.java new file mode 100644 index 0000000000..aefa94a8dc --- /dev/null +++ b/src/java/org/apache/cassandra/utils/btree/BTreeMultimap.java @@ -0,0 +1,214 @@ +/* + * 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.utils.btree; + +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultiset; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +import static java.util.Comparator.naturalOrder; + +public class BTreeMultimap implements Multimap +{ + private final BTreeMap> map; + private final Comparator comparator; + private final Comparator valueComparator; + private final int size; + + private BTreeMultimap(BTreeMap> map, Comparator comparator, Comparator valueComparator, int size) + { + this.map = map; + this.comparator = comparator; + this.valueComparator = valueComparator; + this.size = size; + } + + public static , V extends Comparable> BTreeMultimap empty() + { + return new BTreeMultimap(BTreeMap.empty(), naturalOrder(), naturalOrder(), 0); + } + + public BTreeMultimap with(K key, V value) + { + if (map.containsKey(key)) + { + BTreeSet oldSet = (BTreeSet) map.get(key); + BTreeSet newSet = oldSet.with(value); + int newSize = size + newSet.size() - oldSet.size(); + return new BTreeMultimap<>(map.without(key).with(key, newSet), comparator, valueComparator, newSize); + } + else + { + BTreeSet newSet = BTreeSet.of(valueComparator, value); + return new BTreeMultimap<>(map.with(key, newSet), comparator, valueComparator, size + 1); + } + } + + public BTreeMultimap without(K key) + { + Collection oldSet = map.get(key); + if (oldSet == null) + return this; + int newSize = size - oldSet.size(); + return new BTreeMultimap<>(map.without(key), comparator, valueComparator, newSize); + } + + public BTreeMultimap without(K key, V value) + { + BTreeSet values = (BTreeSet) map.get(key); + if (values == null) + return this; + if (!values.contains(value)) + return this; + BTreeSet newValues = BTreeSet.wrap(BTreeRemoval.remove(values.tree, valueComparator, value), valueComparator); + BTreeMap> newMap = map.without(key); + if (newValues.isEmpty()) + return new BTreeMultimap<>(newMap, comparator, valueComparator, size - 1); + + return new BTreeMultimap<>(newMap.with(key, newValues), comparator, valueComparator, size - 1); + } + + @Override + public int size() + { + return size; + } + + @Override + public boolean isEmpty() + { + return map.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object o) + { + if (o == null) + return false; + return map.containsKey(o); + } + + @Override + public boolean containsValue(@Nullable Object o) + { + if (o == null) + return false; + for (Map.Entry> e : map.entrySet()) + if (e.getValue().contains(o)) + return true; + return false; + } + + @Override + public boolean containsEntry(@Nullable Object key, @Nullable Object value) + { + if (key == null || value == null) + throw new NullPointerException(); + return map.containsKey(key) && map.get(key).contains(value); + } + + @Override + public Collection get(@Nullable K k) + { + if (k == null) + return null; + return map.get(k); + } + + @Override + public Set keySet() + { + return map.keySet(); + } + + @Override + public Multiset keys() + { + ImmutableMultiset.Builder keys = ImmutableMultiset.builder(); + keys.addAll(map.keySet()); + return keys.build(); + } + + @Override + public Collection values() + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Map.Entry> entry : map.entrySet()) + builder.addAll(entry.getValue()); + return builder.build(); + } + + @Override + public Collection> entries() + { + Set> entries = new HashSet<>(); + for (Map.Entry> entry : map.entrySet()) + for (V v : entry.getValue()) + entries.add(new AbstractBTreeMap.Entry<>(entry.getKey(), v)); + return entries; + } + + @Override + public Map> asMap() + { + return map; + } + + public boolean put(@Nullable K k, @Nullable V v) { throw new UnsupportedOperationException();} + public boolean remove(@Nullable Object o, @Nullable Object o1) {throw new UnsupportedOperationException();} + public boolean putAll(@Nullable K k, Iterable iterable) {throw new UnsupportedOperationException();} + public boolean putAll(Multimap multimap) {throw new UnsupportedOperationException();} + public Collection replaceValues(@Nullable K k, Iterable iterable) {throw new UnsupportedOperationException();} + public Collection removeAll(@Nullable Object o) {throw new UnsupportedOperationException();} + public void clear() { throw new UnsupportedOperationException(); } + + @Override + public String toString() + { + return map.toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof BTreeMultimap)) return false; + BTreeMultimap that = (BTreeMultimap) o; + return size == that.size && + Objects.equals(map, that.map) && + Objects.equals(comparator, that.comparator) && + Objects.equals(valueComparator, that.valueComparator); + } + + @Override + public int hashCode() + { + return Objects.hash(map, comparator, valueComparator, size); + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java index b20729d7da..5bb3ac0446 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTreeSet.java +++ b/src/java/org/apache/cassandra/utils/btree/BTreeSet.java @@ -18,7 +18,18 @@ */ package org.apache.cassandra.utils.btree; -import java.util.*; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.SortedSet; +import java.util.Spliterator; +import java.util.Spliterators; import com.google.common.collect.Ordering; @@ -27,7 +38,7 @@ import org.apache.cassandra.utils.btree.BTree.Dir; import static org.apache.cassandra.utils.btree.BTree.findIndex; -public class BTreeSet implements NavigableSet, List +public class BTreeSet extends AbstractSet implements NavigableSet, List { protected final Comparator comparator; protected final Object[] tree; @@ -175,12 +186,16 @@ public class BTreeSet implements NavigableSet, List @Override public V first() { + if (isEmpty()) + throw new NoSuchElementException(); return get(0); } @Override public V last() { + if (isEmpty()) + throw new NoSuchElementException(); return get(size() - 1); } @@ -223,7 +238,6 @@ public class BTreeSet implements NavigableSet, List return false; return true; } - public int hashCode() { // we can't just delegate to Arrays.deepHashCode(), @@ -234,6 +248,7 @@ public class BTreeSet implements NavigableSet, List return result; } + @Override public boolean addAll(Collection c) { @@ -590,24 +605,24 @@ public class BTreeSet implements NavigableSet, List public Builder add(V v) { - wrapped .add(v); + wrapped.add(v); return this; } public Builder addAll(Collection iter) { - wrapped .addAll(iter); + wrapped.addAll(iter); return this; } public boolean isEmpty() { - return wrapped .isEmpty(); + return wrapped.isEmpty(); } public BTreeSet build() { - return new BTreeSet<>(wrapped .build(), wrapped .comparator); + return new BTreeSet<>(wrapped.build(), wrapped.comparator); } } @@ -627,6 +642,22 @@ public class BTreeSet implements NavigableSet, List return new BTreeSet<>(btree, comparator); } + public BTreeSet with(Collection updateWith) + { + Object[] with = BTreeSet.builder(comparator).addAll(updateWith).build().tree; + return new BTreeSet<>(BTree.update(tree, with, comparator, UpdateFunction.noOp()), comparator); + } + + public BTreeSet with(V updateWith) + { + return new BTreeSet<>(BTree.update(tree, new Object[] { updateWith }, comparator, UpdateFunction.noOp()), comparator); + } + + public BTreeSet without(V element) + { + return new BTreeSet<>(BTreeRemoval.remove(tree, comparator, element), comparator); + } + public static > BTreeSet of(Collection sortedValues) { return new BTreeSet<>(BTree.build(sortedValues), Ordering.natural()); diff --git a/src/java/org/apache/cassandra/utils/concurrent/AbstractFuture.java b/src/java/org/apache/cassandra/utils/concurrent/AbstractFuture.java index 0020e1a111..7cf577d66a 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/AbstractFuture.java +++ b/src/java/org/apache/cassandra/utils/concurrent/AbstractFuture.java @@ -383,6 +383,39 @@ public abstract class AbstractFuture implements Future return result; } + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + * + * See {@link #addListener(GenericFutureListener)} for ordering semantics. + */ + @Override + public Future andThenAsync(Function> andThen) + { + return andThenAsync(andThen, null); + } + + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + * + * See {@link #addListener(GenericFutureListener)} for ordering semantics. + */ + protected Future andThenAsync(AbstractFuture result, Function> andThen, @Nullable Executor executor) + { + addListener(() -> { + try + { + if (isSuccess()) andThen.apply(getNow()).addListener(propagate(result)); + else result.tryFailure(cause()); + } + catch (Throwable t) + { + result.tryFailure(t); + throw t; + } + }, executor); + return result; + } + /** * Add a listener to be invoked once this future completes. * Listeners are submitted to {@link #notifyExecutor} in the order they are added (or the specified executor diff --git a/src/java/org/apache/cassandra/utils/concurrent/AsyncFuture.java b/src/java/org/apache/cassandra/utils/concurrent/AsyncFuture.java index 0ef35d5dde..89cea2dbe3 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/AsyncFuture.java +++ b/src/java/org/apache/cassandra/utils/concurrent/AsyncFuture.java @@ -144,6 +144,17 @@ public class AsyncFuture extends AbstractFuture return flatMap(new AsyncFuture<>(), flatMapper, executor); } + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + * + * See {@link #addListener(GenericFutureListener)} for ordering semantics. + */ + @Override + public Future andThenAsync(Function> andThen, @Nullable Executor executor) + { + return andThenAsync(new AsyncFuture<>(), andThen, executor); + } + /** * Wait for this future to complete {@link Awaitable#await()} */ diff --git a/src/java/org/apache/cassandra/utils/concurrent/Future.java b/src/java/org/apache/cassandra/utils/concurrent/Future.java index 3b3c017b7d..c5fc022ac4 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/Future.java +++ b/src/java/org/apache/cassandra/utils/concurrent/Future.java @@ -179,6 +179,16 @@ public interface Future extends io.netty.util.concurrent.Future, Listenabl */ Future flatMap(Function> flatMapper, Executor executor); + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + */ + Future andThenAsync(Function> andThen); + + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + */ + Future andThenAsync(Function> andThen, Executor executor); + /** * Invoke {@code runnable} on completion, using {@code executor}. * diff --git a/src/java/org/apache/cassandra/utils/concurrent/LoadingMap.java b/src/java/org/apache/cassandra/utils/concurrent/LoadingMap.java index 399eb0e3b5..1d41a97db8 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/LoadingMap.java +++ b/src/java/org/apache/cassandra/utils/concurrent/LoadingMap.java @@ -18,6 +18,8 @@ package org.apache.cassandra.utils.concurrent; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; @@ -199,4 +201,9 @@ public class LoadingMap return (T) value; } } + + public Map> copyInternal() + { + return new HashMap>(internalMap); + } } diff --git a/src/java/org/apache/cassandra/utils/concurrent/SyncFuture.java b/src/java/org/apache/cassandra/utils/concurrent/SyncFuture.java index 2a3598aa03..287911bd12 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/SyncFuture.java +++ b/src/java/org/apache/cassandra/utils/concurrent/SyncFuture.java @@ -111,6 +111,17 @@ public class SyncFuture extends AbstractFuture return flatMap(new SyncFuture<>(), flatMapper, executor); } + /** + * Support {@link com.google.common.util.concurrent.Futures#transformAsync(ListenableFuture, AsyncFunction, Executor)} natively + * + * See {@link #addListener(GenericFutureListener)} for ordering semantics. + */ + @Override + public Future andThenAsync(Function> andThen, @Nullable Executor executor) + { + return andThenAsync(new SyncFuture<>(), andThen, executor); + } + /** * Shared implementation of various promise completion methods. * Updates the result if it is possible to do so, returning success/failure. diff --git a/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java b/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java index e9dcdf86e9..eb1c92ccaa 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java +++ b/src/java/org/apache/cassandra/utils/concurrent/WaitQueue.java @@ -159,6 +159,7 @@ public interface WaitQueue return new Standard(); } + @Shared(scope = SIMULATION) class Standard implements WaitQueue { private static final int CANCELLED = -1; diff --git a/test/conf/logback-dtest.xml b/test/conf/logback-dtest.xml index 52eaf335de..48d9859b67 100644 --- a/test/conf/logback-dtest.xml +++ b/test/conf/logback-dtest.xml @@ -27,7 +27,7 @@ ./build/test/logs/${cassandra.testtag}/${suitename}/${cluster_id}/${instance_id}/system.log - %-5level [%thread] ${instance_id} %date{ISO8601} %msg%n + %-5level [%thread] ${instance_id} %date{ISO8601} %F:%L - %msg%n true @@ -50,7 +50,8 @@ - + + diff --git a/test/conf/logback-simulator.xml b/test/conf/logback-simulator.xml index 25c9de8a6a..3a9fabc380 100644 --- a/test/conf/logback-simulator.xml +++ b/test/conf/logback-simulator.xml @@ -40,7 +40,6 @@ - diff --git a/test/distributed/org/apache/cassandra/distributed/Cluster.java b/test/distributed/org/apache/cassandra/distributed/Cluster.java index b7b9207654..788171dd9e 100644 --- a/test/distributed/org/apache/cassandra/distributed/Cluster.java +++ b/test/distributed/org/apache/cassandra/distributed/Cluster.java @@ -21,10 +21,13 @@ package org.apache.cassandra.distributed; import java.io.IOException; import java.util.function.Consumer; -import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.api.*; import org.apache.cassandra.distributed.impl.AbstractCluster; -import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.impl.Instance; import org.apache.cassandra.distributed.shared.Versions; +import org.apache.cassandra.net.Message; /** * A simple cluster supporting only the 'current' Cassandra version, offering easy access to the convenience methods @@ -70,5 +73,39 @@ public class Cluster extends AbstractCluster withVersion(CURRENT_VERSION); } } + + public void enableMessageLogging() + { + filters().allVerbs().inbound().messagesMatching((from, to, msg) -> { + if (!get(1).isShutdown()) + { + get(1).acceptsOnInstance((IIsolatedExecutor.SerializableConsumer) (msgPassed) -> { + Message decoded = Instance.deserializeMessage(msgPassed); + if (!decoded.verb().toString().toLowerCase().contains("gossip")) + System.out.println(String.format("MSG %d -> %d: %s | %s", from, to, decoded, decoded.payload)); + }).accept(msg); + } + return false; + }).drop().on(); + } + + @Override + public void disableAutoCompaction(String keyspace) + { + stream().forEach(i -> { + i.acceptsOnInstance(new DisableAutoCompaction()).accept(keyspace); + }); + } + + // Without this class, lambda is trying to capture too much of the Cluster object, which leads to + // an attempt to capture unshareable class instances. + private static class DisableAutoCompaction implements IIsolatedExecutor.SerializableConsumer + { + public void accept(String ks) + { + for (ColumnFamilyStore cs : Keyspace.open(ks).getColumnFamilyStores()) + cs.disableAutoCompaction(); + } + } } diff --git a/test/distributed/org/apache/cassandra/distributed/Constants.java b/test/distributed/org/apache/cassandra/distributed/Constants.java index d7a2acd11c..cdd8843d8b 100644 --- a/test/distributed/org/apache/cassandra/distributed/Constants.java +++ b/test/distributed/org/apache/cassandra/distributed/Constants.java @@ -38,4 +38,8 @@ public final class Constants * 'true'. */ public static final String KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN = "dtest.api.startup.failure_as_shutdown"; + + public static final String KEY_DTEST_FULL_STARTUP = "dtest.api.startup.full_startup"; + + public static final String KEY_DTEST_JOIN_RING = "dtest.api.startup.join_ring"; } diff --git a/test/distributed/org/apache/cassandra/distributed/action/GossipHelper.java b/test/distributed/org/apache/cassandra/distributed/action/GossipHelper.java index 7a46935646..e41b3eca52 100644 --- a/test/distributed/org/apache/cassandra/distributed/action/GossipHelper.java +++ b/test/distributed/org/apache/cassandra/distributed/action/GossipHelper.java @@ -18,16 +18,11 @@ package org.apache.cassandra.distributed.action; -import java.io.IOException; -import java.io.Serializable; import java.net.InetSocketAddress; -import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -38,22 +33,17 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.shared.ThrowingRunnable; import org.apache.cassandra.distributed.shared.VersionedApplicationState; 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.io.util.DataInputBuffer; -import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.toCassandraInetAddressAndPort; -import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; -import static org.junit.Assert.assertTrue; public class GossipHelper { @@ -62,30 +52,6 @@ public class GossipHelper return (instance) -> changeGossipState(instance, newNode,Collections.emptyList()); } - public static InstanceAction statusToBootstrap(IInvokableInstance newNode) - { - return (instance) -> - { - changeGossipState(instance, - newNode, - Arrays.asList(tokens(newNode), - statusBootstrapping(newNode), - statusWithPortBootstrapping(newNode))); - }; - } - - public static InstanceAction statusToDecommission(IInvokableInstance newNode) - { - return (instance) -> - { - changeGossipState(instance, - newNode, - Arrays.asList(tokens(newNode), - statusLeaving(newNode), - statusWithPortLeaving(newNode))); - }; - } - public static InstanceAction statusToNormal(IInvokableInstance peer) { return (target) -> @@ -134,180 +100,12 @@ public class GossipHelper peer.getReleaseVersionString()))); } - public static InstanceAction statusToLeaving(IInvokableInstance newNode) - { - return (instance) -> { - changeGossipState(instance, - newNode, - Arrays.asList(tokens(newNode), - statusLeaving(newNode), - statusWithPortLeaving(newNode))); - }; - } - - public static InstanceAction bootstrap() - { - return new BootstrapAction(); - } - - public static InstanceAction bootstrap(boolean joinRing, Duration waitForBootstrap, Duration waitForSchema) - { - return new BootstrapAction(joinRing, waitForBootstrap, waitForSchema); - } - - public static InstanceAction disseminateGossipState(IInvokableInstance newNode) - { - return new DisseminateGossipState(newNode); - } - - public static InstanceAction pullSchemaFrom(IInvokableInstance pullFrom) - { - return new PullSchemaFrom(pullFrom); - } - - private static InstanceAction disableBinary() - { - return (instance) -> instance.nodetoolResult("disablebinary").asserts().success(); - } - - private static class DisseminateGossipState implements InstanceAction - { - final Map gossipState; - - public DisseminateGossipState(IInvokableInstance... from) - { - gossipState = new HashMap<>(); - for (IInvokableInstance node : from) - { - byte[] epBytes = node.callsOnInstance(() -> { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(FBUtilities.getBroadcastAddressAndPort()); - return toBytes(epState); - }).call(); - gossipState.put(node.broadcastAddress(), epBytes); - } - } - - public void accept(IInvokableInstance instance) - { - instance.appliesOnInstance((IIsolatedExecutor.SerializableFunction, Void>) - (map) -> { - Map newState = new HashMap<>(); - for (Map.Entry e : map.entrySet()) - newState.put(toCassandraInetAddressAndPort(e.getKey()), fromBytes(e.getValue())); - - Gossiper.runInGossipStageBlocking(() -> { - Gossiper.instance.applyStateLocally(newState); - }); - return null; - }).apply(gossipState); - } - } - - private static byte[] toBytes(EndpointState epState) - { - try (DataOutputBuffer out = new DataOutputBuffer(1024)) - { - EndpointState.serializer.serialize(epState, out, MessagingService.current_version); - return out.toByteArray(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - private static EndpointState fromBytes(byte[] bytes) - { - try (DataInputBuffer in = new DataInputBuffer(bytes)) - { - return EndpointState.serializer.deserialize(in, MessagingService.current_version); - } - catch (Throwable t) - { - throw new RuntimeException(t); - } - } - - private static class PullSchemaFrom implements InstanceAction - { - final InetSocketAddress pullFrom; - - public PullSchemaFrom(IInvokableInstance pullFrom) - { - this.pullFrom = pullFrom.broadcastAddress();; - } - - public void accept(IInvokableInstance pullTo) - { - pullTo.acceptsOnInstance((InetSocketAddress pullFrom) -> { - InetAddressAndPort endpoint = toCassandraInetAddressAndPort(pullFrom); - EndpointState state = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - Gossiper.instance.doOnChangeNotifications(endpoint, ApplicationState.SCHEMA, state.getApplicationState(ApplicationState.SCHEMA)); - assertTrue("schema is ready", Schema.instance.waitUntilReady(Duration.ofSeconds(10))); - }).accept(pullFrom); - } - } - - private static class BootstrapAction implements InstanceAction, Serializable - { - private final boolean joinRing; - private final Duration waitForBootstrap; - private final Duration waitForSchema; - - public BootstrapAction() - { - this(true, Duration.ofMinutes(10), Duration.ofSeconds(10)); - } - - public BootstrapAction(boolean joinRing, Duration waitForBootstrap, Duration waitForSchema) - { - this.joinRing = joinRing; - this.waitForBootstrap = waitForBootstrap; - this.waitForSchema = waitForSchema; - } - - public void accept(IInvokableInstance instance) - { - instance.appliesOnInstance((String partitionerString, String tokenString) -> { - IPartitioner partitioner = FBUtilities.newPartitioner(partitionerString); - List tokens = Collections.singletonList(partitioner.getTokenFactory().fromString(tokenString)); - try - { - Collection collisions = StorageService.instance.prepareForBootstrap(waitForSchema.toMillis(), 0); - assert collisions.size() == 0 : String.format("Didn't expect any replacements but got %s", collisions); - boolean isBootstrapSuccessful = StorageService.instance.bootstrap(tokens, waitForBootstrap.toMillis()); - assert isBootstrapSuccessful : "Bootstrap did not complete successfully"; - StorageService.instance.setUpDistributedSystemKeyspaces(); - if (joinRing) - StorageService.instance.finishJoiningRing(true, tokens); - } - catch (Throwable t) - { - throw new RuntimeException(t); - } - - return null; - }).apply(instance.config().getString("partitioner"), instance.config().getString("initial_token")); - } - } - - public static InstanceAction decommission() - { - return decommission(false); - } - - public static InstanceAction decommission(boolean force) - { - return force ? (target) -> target.nodetoolResult("decommission", "--force").asserts().success() - : (target) -> target.nodetoolResult("decommission").asserts().success(); - } - - public static VersionedApplicationState tokens(IInvokableInstance instance) + private static VersionedApplicationState tokens(IInvokableInstance instance) { return versionedToken(instance, ApplicationState.TOKENS, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).tokens(tokens)); } - public static VersionedApplicationState netVersion(IInvokableInstance instance) + private static VersionedApplicationState netVersion(IInvokableInstance instance) { return versionedToken(instance, ApplicationState.NET_VERSION, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).networkVersion()); } @@ -317,62 +115,27 @@ public class GossipHelper return unsafeVersionedValue(instance, ApplicationState.RELEASE_VERSION, (partitioner) -> new VersionedValue.VersionedValueFactory(partitioner).releaseVersion(releaseVersionStr), partitionerStr); } - public static VersionedApplicationState releaseVersion(IInvokableInstance instance) + private static VersionedApplicationState releaseVersion(IInvokableInstance instance) { return unsafeReleaseVersion(instance, instance.config().getString("partitioner"), instance.getReleaseVersionString()); } - public static VersionedApplicationState statusNormal(IInvokableInstance instance) + private static VersionedApplicationState statusNormal(IInvokableInstance instance) { return versionedToken(instance, ApplicationState.STATUS, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).normal(tokens)); } - public static VersionedApplicationState statusWithPortNormal(IInvokableInstance instance) + private static VersionedApplicationState statusWithPortNormal(IInvokableInstance instance) { return versionedToken(instance, ApplicationState.STATUS_WITH_PORT, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).normal(tokens)); } - public static VersionedApplicationState statusBootstrapping(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).bootstrapping(tokens)); - } - - public static VersionedApplicationState statusWithPortBootstrapping(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS_WITH_PORT, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).bootstrapping(tokens)); - } - - public static VersionedApplicationState statusLeaving(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).leaving(tokens)); - } - - public static VersionedApplicationState statusLeft(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS, (partitioner, tokens) -> { - return new VersionedValue.VersionedValueFactory(partitioner).left(tokens, currentTimeMillis() + Gossiper.aVeryLongTime); - }); - } - - public static VersionedApplicationState statusWithPortLeft(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS_WITH_PORT, (partitioner, tokens) -> { - return new VersionedValue.VersionedValueFactory(partitioner).left(tokens, currentTimeMillis() + Gossiper.aVeryLongTime); - - }); - } - - public static VersionedApplicationState statusWithPortLeaving(IInvokableInstance instance) - { - return versionedToken(instance, ApplicationState.STATUS_WITH_PORT, (partitioner, tokens) -> new VersionedValue.VersionedValueFactory(partitioner).leaving(tokens)); - } - - public static VersionedValue toVersionedValue(VersionedApplicationState vv) + private static VersionedValue toVersionedValue(VersionedApplicationState vv) { return VersionedValue.unsafeMakeVersionedValue(vv.value, vv.version); } - public static ApplicationState toApplicationState(VersionedApplicationState vv) + private static ApplicationState toApplicationState(VersionedApplicationState vv) { return ApplicationState.values()[vv.applicationState]; } @@ -405,32 +168,15 @@ public class GossipHelper }).apply(partitionerStr); } - public static VersionedApplicationState versionedToken(IInvokableInstance instance, ApplicationState applicationState, IIsolatedExecutor.SerializableBiFunction, VersionedValue> supplier) + private static VersionedApplicationState versionedToken(IInvokableInstance instance, ApplicationState applicationState, IIsolatedExecutor.SerializableBiFunction, VersionedValue> supplier) { return unsafeVersionedValue(instance, applicationState, supplier, instance.config().getString("partitioner"), instance.config().getString("initial_token")); } - public static InstanceAction removeFromRing(IInvokableInstance peer) - { - return (target) -> { - InetAddressAndPort endpoint = toCassandraInetAddressAndPort(peer.broadcastAddress()); - VersionedApplicationState newState = statusLeft(peer); - - target.runOnInstance(() -> { - // state to 'left' - EndpointState currentState = Gossiper.instance.getEndpointStateForEndpoint(endpoint); - ApplicationState as = toApplicationState(newState); - VersionedValue vv = toVersionedValue(newState); - currentState.addApplicationState(as, vv); - StorageService.instance.onChange(endpoint, as, vv); - }); - }; - } - /** * Changes gossip state of the `peer` on `target` */ - public static void changeGossipState(IInvokableInstance target, IInstance peer, List newState) + private static void changeGossipState(IInvokableInstance target, IInstance peer, List newState) { InetSocketAddress addr = peer.broadcastAddress(); UUID hostId = peer.config().hostId(); @@ -468,18 +214,22 @@ public class GossipHelper return netVersion; } - public static void withProperty(CassandraRelevantProperties prop, boolean value, Runnable r) + public static void withProperty(CassandraRelevantProperties prop, boolean value, ThrowingRunnable r) { withProperty(prop, Boolean.toString(value), r); } - public static void withProperty(CassandraRelevantProperties prop, String value, Runnable r) + public static void withProperty(CassandraRelevantProperties prop, String value, ThrowingRunnable r) { String prev = prop.setString(value); try { r.run(); } + catch (Throwable e) + { + throw new RuntimeException(e); + } finally { if (prev == null) diff --git a/test/distributed/org/apache/cassandra/distributed/api/ICoordinator.java b/test/distributed/org/apache/cassandra/distributed/api/ICoordinator.java index 6415a303de..aee6aeaeb8 100644 --- a/test/distributed/org/apache/cassandra/distributed/api/ICoordinator.java +++ b/test/distributed/org/apache/cassandra/distributed/api/ICoordinator.java @@ -27,6 +27,28 @@ import org.apache.cassandra.distributed.shared.FutureUtils; // The cross-version API requires that a Coordinator can be constructed without any constructor arguments public interface ICoordinator { + int RETRIES = 10; + + default Object[][] executeWithRetries(String query, ConsistencyLevel consistencyLevel, Object... boundValues) + { + for (int i = 0; i < RETRIES; i++) + { + { + try + { + return executeWithResult(query, consistencyLevel, boundValues).toObjectArrays(); + } + catch (Throwable t) + { + if (t.getClass().getName().contains("Timeout")) + continue; + throw t; + } + } + } + throw new IllegalStateException(String.format("Did not suceed with query after %d retries", RETRIES)); + } + default Object[][] execute(String query, ConsistencyLevel consistencyLevel, Object... boundValues) { return executeWithResult(query, consistencyLevel, boundValues).toObjectArrays(); diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java new file mode 100644 index 0000000000..8c43b46a84 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java @@ -0,0 +1,123 @@ +/* + * 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.fuzz; + +import java.io.File; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.ddl.SchemaSpec; +import harry.generators.Surjections; +import harry.model.sut.injvm.InJvmSut; +import harry.runner.LockingDataTracker; +import harry.runner.Runner; +import harry.visitors.MutatingVisitor; +import harry.visitors.QueryLogger; +import harry.visitors.RandomPartitionValidator; +import org.apache.cassandra.distributed.api.ICluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.test.TestBaseImpl; + +import static harry.core.Configuration.VisitorPoolConfiguration.pool; +import static java.util.Arrays.asList; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +public class ConcurrentQuiescentCheckerIntegrationTest extends TestBaseImpl +{ + + private static Supplier schemaSupplier() + { + Surjections.Surjection schemaGen = HarryHelper.schemaSpecGen("harry", "table_"); + return new Supplier() + { + int schemaIdx = 0; + public SchemaSpec get() + { + while (true) + { + SchemaSpec schema = schemaGen.inflate(schemaIdx++); + try + { + schema.validate(); + return schema; + } + catch (AssertionError e) + { + continue; + } + } + } + }; + } + + @Test + public void testWithConcurrentQuiescentChecker() throws Throwable + { + final int writeThreads = 2; + final int readThreads = 2; + + try (ICluster cluster = builder().withNodes(3) + .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL)) + .start()) + { + Supplier schemaSupplier = schemaSupplier(); + + for (int i = 0; i < 3; i++) + { + SchemaSpec schema = schemaSupplier.get(); + Configuration config = HarryHelper + .defaultConfiguration() + .setSeed(1L) + .setClock(new Configuration.ApproximateMonotonicClockConfiguration((int) TimeUnit.MINUTES.toSeconds(20), 1, TimeUnit.SECONDS)) + .setSUT(() -> new InJvmSut(cluster)) + .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};") + .setSchemaProvider((seed, sut) -> schema) + .setCreateSchema(true) + .setDropSchema(true) + .setDataTracker(LockingDataTracker::new) + .build(); + + Run run = config.createRun(); + try + { + new Runner.ConcurrentRunner(config.createRun(), config, + asList(pool("Writer", writeThreads, MutatingVisitor::new), + pool("Reader", readThreads, (run_) -> new RandomPartitionValidator(run_, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))), + 1, TimeUnit.MINUTES) + .run(); + } + catch (Throwable e) + { + Configuration.toFile(new File(String.format("failure-%d.yaml", config.seed)), + config.unbuild() + .setClock(run.clock.toConfig()) + .setDataTracker(run.tracker.toConfig()) + .build()); + throw e; + } + } + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java b/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java index 66178f230d..6c79573873 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java @@ -20,8 +20,13 @@ package org.apache.cassandra.distributed.fuzz; import java.io.File; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import harry.core.Configuration; +import harry.ddl.ColumnSpec; +import harry.ddl.SchemaGenerators; +import harry.ddl.SchemaSpec; +import harry.generators.Surjections; import harry.model.OpSelectors; import harry.model.clock.OffsetClock; import harry.model.sut.PrintlnSut; @@ -64,6 +69,50 @@ public class HarryHelper return configuration; } + private static AtomicInteger counter = new AtomicInteger(); + + public static Surjections.Surjection schemaSpecGen(String keyspace, String prefix) + { + return new SchemaGenerators.Builder(keyspace, () -> prefix + counter.getAndIncrement()) + .partitionKeySpec(1, 2, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.doubleType, + ColumnSpec.asciiType, + ColumnSpec.textType) + .clusteringKeySpec(1, 2, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.doubleType, + ColumnSpec.asciiType, + ColumnSpec.textType, + ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), + ColumnSpec.ReversedType.getInstance(ColumnSpec.doubleType), + ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType), + ColumnSpec.ReversedType.getInstance(ColumnSpec.textType)) + .regularColumnSpec(1, 5, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType(4, 128)) + .staticColumnSpec(0, 5, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType(4, 128)) + .surjection(); + } + public static Configuration.ConfigurationBuilder defaultConfiguration() throws Exception { return new Configuration.ConfigurationBuilder() @@ -71,7 +120,7 @@ public class HarryHelper .setCreateSchema(true) .setTruncateTable(false) .setDropSchema(false) - .setSchemaProvider(new Configuration.DefaultSchemaProviderConfiguration()) + .setSchemaProvider((seed, sut) -> schemaSpecGen("harry", "tbl_").inflate(seed)) .setClock(new Configuration.ApproximateMonotonicClockConfiguration(7300, 1, TimeUnit.SECONDS)) .setClusteringDescriptorSelector(defaultClusteringDescriptorSelectorConfiguration().build()) .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(100, 10)) @@ -86,8 +135,8 @@ public class HarryHelper public static Configuration.CDSelectorConfigurationBuilder defaultClusteringDescriptorSelectorConfiguration() { return new Configuration.CDSelectorConfigurationBuilder() - .setNumberOfModificationsDistribution(new Configuration.ConstantDistributionConfig(2)) - .setRowsPerModificationDistribution(new Configuration.ConstantDistributionConfig(2)) + .setNumberOfModificationsDistribution(new Configuration.ConstantDistributionConfig(1)) + .setRowsPerModificationDistribution(new Configuration.ConstantDistributionConfig(1)) .setMaxPartitionSize(100) .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder() .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1) @@ -102,4 +151,16 @@ public class HarryHelper .addWeight(OpSelectors.OperationKind.UPDATE, 20) .build()); } + + public static Configuration.CDSelectorConfigurationBuilder singleRowPerModification() + { + return new Configuration.CDSelectorConfigurationBuilder() + .setNumberOfModificationsDistribution(new Configuration.ConstantDistributionConfig(1)) + .setRowsPerModificationDistribution(new Configuration.ConstantDistributionConfig(1)) + .setMaxPartitionSize(100) + .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder() + .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 100) + .build()); + } } + diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java b/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java new file mode 100644 index 0000000000..05e48ef25a --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java @@ -0,0 +1,111 @@ +/* + * 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.fuzz; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import harry.core.Run; +import harry.ddl.SchemaSpec; +import harry.model.sut.SystemUnderTest; +import harry.operations.CompiledStatement; +import harry.visitors.*; +import org.apache.cassandra.distributed.test.log.RngUtils; + +public class InJVMTokenAwareVisitorExecutor extends LoggingVisitor.LoggingVisitorExecutor +{ + public static void init() + { + harry.core.Configuration.registerSubtypes(Configuration.class); + } + + private final InJvmSut sut; + private final SystemUnderTest.ConsistencyLevel cl; + private final SchemaSpec schema; + private final int maxRetries = 10; + + // TODO: as of now, token aware executor only performs operations against natrual, but not pending replicas. + public InJVMTokenAwareVisitorExecutor(Run run, + OperationExecutor.RowVisitorFactory rowVisitorFactory, + SystemUnderTest.ConsistencyLevel cl) + { + super(run, rowVisitorFactory.make(run)); + this.sut = (InJvmSut) run.sut; + this.schema = run.schemaSpec; + this.cl = cl; + } + + @Override + protected void executeAsyncWithRetries(long lts, long pd, CompletableFuture future, CompiledStatement statement) + { + executeAsyncWithRetries(lts, pd, future, statement, 0); + } + + private void executeAsyncWithRetries(long lts, long pd, CompletableFuture future, CompiledStatement statement, int retries) + { + if (sut.isShutdown()) + throw new IllegalStateException("System under test is shut down"); + + if (retries > this.maxRetries) + throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries)); + + Object[] partitionKey = schema.inflatePartitionKey(pd); + int[] replicas = sut.getReadReplicasFor(partitionKey, schema.keyspace, schema.table); + int replica = replicas[RngUtils.asInt(lts, 0, replicas.length - 1)]; + if (cl == SystemUnderTest.ConsistencyLevel.NODE_LOCAL) + { + future.complete(sut.cluster.get(replica).executeInternal(statement.cql(), statement.bindings())); + } + else + { + CompletableFuture.supplyAsync(() -> sut.cluster.coordinator(replica).execute(statement.cql(), InJvmSut.toApiCl(cl), statement.bindings()), executor) + .whenComplete((res, t) -> + { + if (t != null) + executor.schedule(() -> executeAsyncWithRetries(lts, pd, future, statement, retries + 1), 1, TimeUnit.SECONDS); + else + future.complete(res); + }); + } + } + + @JsonTypeName("in_jvm_token_aware") + public static class Configuration implements harry.core.Configuration.VisitorConfiguration + { + public final harry.core.Configuration.RowVisitorConfiguration row_visitor; + public final SystemUnderTest.ConsistencyLevel consistency_level; + + @JsonCreator + public Configuration(@JsonProperty("row_visitor") harry.core.Configuration.RowVisitorConfiguration rowVisitor, + @JsonProperty("consistency_level") SystemUnderTest.ConsistencyLevel consistencyLevel) + { + this.row_visitor = rowVisitor; + this.consistency_level = consistencyLevel; + } + + @Override + public Visitor make(Run run) + { + return new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, row_visitor, consistency_level)); + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java b/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java index d62b2c6ff2..0ba0206afd 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java @@ -20,7 +20,11 @@ package org.apache.cassandra.distributed.fuzz; import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +33,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import harry.core.Configuration; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; public class InJvmSut extends InJvmSutBase { @@ -44,12 +54,12 @@ public class InJvmSut extends InJvmSutBase public InJvmSut(Cluster cluster) { - super(cluster, 10); + super(cluster, roundRobin(cluster), retryOnTimeout(), 10); } - public InJvmSut(Cluster cluster, int threads) + public InJvmSut(Cluster cluster, Supplier lbs, Function retryStrategy) { - super(cluster, threads); + super(cluster, lbs, retryStrategy, 10); } @JsonTypeName("in_jvm") @@ -83,4 +93,31 @@ public class InJvmSut extends InJvmSutBase return new InJvmSut(cluster); } } + + // TODO: this would only return _read_ (or natural) replicas for the token + public int[] getReadReplicasFor(Object[] partitionKey, String keyspace, String table) + { + return cluster.get(1).appliesOnInstance(InJvmSut::getReadReplicasForCallable).apply(partitionKey, keyspace, table); + } + + public static int[] getReadReplicasForCallable(Object[] pk, String ks, String table) + { + String pkString = Arrays.stream(pk).map(Object::toString).collect(Collectors.joining(":")); + EndpointsForToken endpoints = StorageService.instance.getNaturalReplicasForToken(ks, table, pkString); + int[] nodes = new int[endpoints.size()]; + for (int i = 0; i < endpoints.size(); i++) + nodes[i] = endpoints.get(i).endpoint().getAddress().getAddress()[3]; + + sanity_check: + { + Keyspace ksp = Keyspace.open(ks); + Token token = DatabaseDescriptor.getPartitioner().getToken(ksp.getMetadata().getTableOrViewNullable(table).partitionKeyType.fromString(pkString)); + + ClusterMetadata metadata = ClusterMetadata.current(); + EndpointsForToken replicas = metadata.placements.get(ksp.getMetadata().params.replication).reads.forToken(token).get(); + + assert replicas.endpoints().equals(endpoints.endpoints()) : String.format("Consistent metadata endpoints %s disagree with token metadata computation %s", endpoints.endpoints(), replicas.endpoints()); + } + return nodes; + } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java b/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java index 585fc81d3d..64a961e3a8 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java +++ b/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java @@ -30,6 +30,8 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; import com.google.common.collect.Iterators; import org.apache.commons.lang3.StringUtils; @@ -58,20 +60,46 @@ public class InJvmSutBase private final ExecutorService executor; public final CLUSTER cluster; - private final AtomicLong cnt = new AtomicLong(); private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final Supplier loadBalancingStrategy; + private final Function retryStrategy; public InJvmSutBase(CLUSTER cluster) { - this(cluster, 10); + this(cluster, roundRobin(cluster), retryOnTimeout(), 10); } - public InJvmSutBase(CLUSTER cluster, int threads) + public InJvmSutBase(CLUSTER cluster, Supplier loadBalancingStrategy, Function retryStrategy, int threads) { this.cluster = cluster; this.executor = Executors.newFixedThreadPool(threads); + this.loadBalancingStrategy = loadBalancingStrategy; + this.retryStrategy = retryStrategy; } + public static Supplier roundRobin(ICluster cluster) + { + return new Supplier() + { + private final AtomicLong cnt = new AtomicLong(); + + public Integer get() + { + return (int) (cnt.getAndIncrement() % cluster.size() + 1); + } + }; + } + + public static Function retryOnTimeout() + { + return new Function() + { + public Boolean apply(Throwable t) + { + return t.getMessage().contains("timed out"); + } + }; + } public CLUSTER cluster() { return cluster; @@ -109,7 +137,7 @@ public class InJvmSutBase public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) { - return execute(statement, cl, (int) (cnt.getAndIncrement() % cluster.size() + 1), bindings); + return execute(statement, cl, loadBalancingStrategy.get(), bindings); } public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, Object... bindings) @@ -142,9 +170,8 @@ public class InJvmSutBase } catch (Throwable t) { - // TODO: find a better way to work around timeouts - if (t.getMessage().contains("timed out")) - return execute(statement, cl, coordinator, bindings); + if (retryStrategy.apply(t)) + return execute(statement, cl, bindings); logger.error(String.format("Caught error while trying execute statement %s (%s): %s", statement, Arrays.toString(bindings), t.getMessage()), @@ -161,7 +188,7 @@ public class InJvmSutBase try { - int coordinator = (int) (cnt.getAndIncrement() % cluster.size() + 1); + int coordinator = loadBalancingStrategy.get(); IMessageFilters filters = cluster.filters(); // Drop exactly one coordinated message diff --git a/test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java b/test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java new file mode 100644 index 0000000000..3cb3c7661b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java @@ -0,0 +1,24 @@ +/* + * 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.harry; + +public interface ClusterState +{ + boolean isDown(int i); +} diff --git a/test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java b/test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java new file mode 100644 index 0000000000..9037dd92cf --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java @@ -0,0 +1,96 @@ +/* + * 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.harry; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import com.google.common.util.concurrent.Uninterruptibles; + +import harry.core.Configuration; +import harry.model.sut.SystemUnderTest; +import org.apache.cassandra.distributed.api.ICluster; + +public class ExistingClusterSUT implements Configuration.SutConfiguration +{ + private final ICluster cluster; + private final ClusterState clusterState; + + public ExistingClusterSUT(ICluster cluster, ClusterState clusterState) + { + this.cluster = cluster; + this.clusterState = clusterState; + } + + @Override + public SystemUnderTest make() + { + return new SystemUnderTest() + { + int toQuery = 0; + @Override + public boolean isShutdown() + { + return false; + } + + @Override + public void shutdown() + { + } + + @Override + public void schemaChange(String schemaChange) + { + cluster.schemaChange(schemaChange); + } + + @Override + public Object[][] execute(String s, SystemUnderTest.ConsistencyLevel consistencyLevel, Object... objects) + { + Exception lastEx = null; + for (int i = 0; i < 20; i++) + { + toQuery++; + if (cluster.size() == 0) + continue; + int coordinator = (toQuery % cluster.size()) + 1; + if (clusterState.isDown(coordinator)) + continue; + try + { + return cluster.coordinator(coordinator).execute(s, org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM, objects); + } + catch (Exception e) + { + lastEx = e; + Uninterruptibles.sleepUninterruptibly(50, TimeUnit.MILLISECONDS); + } + } + throw new RuntimeException(lastEx); + } + + @Override + public CompletableFuture executeAsync(String s, SystemUnderTest.ConsistencyLevel consistencyLevel, Object... objects) + { + return CompletableFuture.supplyAsync(() -> this.execute(s, consistencyLevel, objects)); + } + }; + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java b/test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java new file mode 100644 index 0000000000..b39c7eb51f --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java @@ -0,0 +1,85 @@ +/* + * 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.harry; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import harry.concurrent.ExecutorFactory; +import harry.concurrent.Interruptible; +import harry.core.Configuration; +import harry.core.Run; +import harry.runner.Runner; +import harry.visitors.Visitor; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static harry.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; +import static harry.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; +import static harry.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; + +public class FlaggedRunner extends Runner +{ + private final List poolConfigurations; + private final CountDownLatch stopLatch; + + public FlaggedRunner(Run run, Configuration config, List poolConfigurations, CountDownLatch stopLatch) + { + super(run, config); + this.poolConfigurations = poolConfigurations; + this.stopLatch = stopLatch; + } + + @Override + protected void runInternal() throws Throwable + { + List threads = new ArrayList<>(); + Map counters = new HashMap<>(); + for (Configuration.VisitorPoolConfiguration poolConfiguration : poolConfigurations) + { + for (int i = 0; i < poolConfiguration.concurrency; i++) + { + Visitor visitor = poolConfiguration.visitor.make(run); + String name = String.format("%s-%d", poolConfiguration.prefix, i + 1); + counters.put(name, 0); + Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(name, wrapInterrupt((state) -> { + if (state == Interruptible.State.NORMAL) + { + visitor.visit(); + counters.compute(name, (n, old) -> old + 1); + } + }, stopLatch::decrement, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); + threads.add(thread); + } + } + + stopLatch.await(); + shutdown(threads::stream); + System.out.println("counters = " + counters); + if (!errors.isEmpty()) + mergeAndThrow(errors); + } + + @Override + public String type() + { + return "concurrent_flagged"; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java index c7e4c10521..4979f7f113 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/AbstractCluster.java @@ -61,8 +61,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Constants; @@ -806,7 +804,12 @@ public abstract class AbstractCluster implements ICluster implements ICluster { - for (ColumnFamilyStore cs : Keyspace.open(keyspace).getColumnFamilyStores()) - cs.disableAutoCompaction(); - }); + forEach((i) -> i.nodetool("disableautocompaction", keyspace)); } public void schemaChange(String query) @@ -934,7 +934,8 @@ public abstract class AbstractCluster implements ICluster implements ICluster { i.startup(this); - i.postStartup(); }); parallelForEach(startParallel, i -> { i.startup(this); - i.postStartup(); }, 0, null); + parallelForEach(instances, IInstance::postStartup, 0, null); monitor.waitForCompletion(); } } @@ -1084,20 +1084,27 @@ public abstract class AbstractCluster implements ICluster !i.isShutdown()) - .map(IInstance::shutdown) - .collect(Collectors.toList()), - 1L, TimeUnit.MINUTES); + + List> futures = new ArrayList<>(); + futures = instances.stream() + .filter(i -> !i.isShutdown()) + .map(IInstance::shutdown) + .collect(Collectors.toList()); + FBUtilities.waitOnFutures(futures,1L, TimeUnit.MINUTES); instances.clear(); instanceMap.clear(); PathUtils.setDeletionListener(ignore -> {}); // Make sure to only delete directory when threads are stopped - if (Files.exists(root)) + if (Files.exists(root) && futures.stream().allMatch(f -> f.isDone())) PathUtils.deleteRecursive(root); + else + logger.error("Not removing directories, as some instances haven't fully stopped."); Thread.setDefaultUncaughtExceptionHandler(previousHandler); previousHandler = null; checkAndResetUncaughtExceptions(); @@ -1207,11 +1214,11 @@ public abstract class AbstractCluster implements ICluster toNames(Set> classes) diff --git a/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java b/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java index 6a892c416b..678af72b7f 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/DistributedTestSnitch.java @@ -64,7 +64,7 @@ public class DistributedTestSnitch extends AbstractNetworkTopologySnitch private Map> savedEndpoints; private static final String DEFAULT_DC = "UNKNOWN_DC"; - private static final String DEFAULT_RACK = "UNKNOWN_RACK"; + private static final String DEFAULT_RACK = "UNKNOWN_RCK"; // TODO must be =< 12 chars to preserve nodetool output required by tests public String getRack(InetAddress endpoint) { diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java index 28a633f256..adf710756c 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java @@ -26,14 +26,16 @@ import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.file.FileSystem; import java.nio.file.FileSystems; -import java.security.Permission; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -45,7 +47,6 @@ import javax.management.Notification; import javax.management.NotificationListener; import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +58,7 @@ import org.apache.cassandra.batchlog.BatchlogManager; import org.apache.cassandra.concurrent.ExecutorFactory; import org.apache.cassandra.concurrent.ExecutorLocals; import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.concurrent.NamedThreadFactory; import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.SharedExecutorPool; import org.apache.cassandra.concurrent.Stage; @@ -70,11 +72,11 @@ import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.SystemKeyspaceMigrator41; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionLogger; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.memtable.AbstractAllocatorMemtable; +import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.action.GossipHelper; @@ -90,8 +92,11 @@ import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.mock.nodetool.InternalNodeProbe; import org.apache.cassandra.distributed.mock.nodetool.InternalNodeProbeFactory; +import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.shared.Metrics; import org.apache.cassandra.distributed.shared.ThrowingRunnable; +import org.apache.cassandra.distributed.test.log.TestProcessor; +import org.apache.cassandra.exceptions.StartupException; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.hints.DTestSerializer; import org.apache.cassandra.hints.HintsService; @@ -112,7 +117,6 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.Verb; -import org.apache.cassandra.schema.MigrationCoordinator; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ActiveRepairService; @@ -120,7 +124,6 @@ import org.apache.cassandra.service.CassandraDaemon; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.DefaultFSErrorHandler; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.StorageServiceMBean; @@ -128,11 +131,17 @@ import org.apache.cassandra.service.paxos.PaxosRepair; import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.uncommitted.UncommittedTableData; import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings; -import org.apache.cassandra.service.snapshot.SnapshotManager; import org.apache.cassandra.streaming.StreamManager; import org.apache.cassandra.streaming.StreamReceiveTask; import org.apache.cassandra.streaming.StreamTransferTask; import org.apache.cassandra.streaming.async.NettyStreamingChannel; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Startup; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.tools.NodeTool; import org.apache.cassandra.tools.Output; import org.apache.cassandra.tools.SystemExitException; @@ -158,6 +167,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.CONSISTENT import static org.apache.cassandra.config.CassandraRelevantProperties.RING_DELAY; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_CASSANDRA_SUITENAME; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_CASSANDRA_TESTTAG; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY; import static org.apache.cassandra.distributed.api.Feature.BLANK_GOSSIP; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.JMX; @@ -166,7 +176,7 @@ import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.fromCassandraInetAddressAndPort; import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.toCassandraInetAddressAndPort; import static org.apache.cassandra.net.Verb.BATCH_STORE_REQ; -import static org.apache.cassandra.utils.Clock.Global.nanoTime; +import static org.apache.cassandra.service.CassandraDaemon.logSystemInfo; /** * This class is instantiated on the relevant classloader, so its methods invoke the correct target classes automatically @@ -253,7 +263,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance public IListen listen() { - return new Listen(this); + return new Listen(); } @Override @@ -332,7 +342,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance }).run(); } - private void registerMockMessaging(ICluster cluster) + protected void registerMockMessaging(ICluster cluster) { MessagingService.instance().outboundSink.add((message, to) -> { if (!internodeMessagingStarted) @@ -346,7 +356,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance }); } - private void registerInboundFilter(ICluster cluster) + protected void registerInboundFilter(ICluster cluster) { MessagingService.instance().inboundSink.add(message -> { if (!cluster.filters().hasInbound()) @@ -363,7 +373,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance }); } - private void registerOutboundFilter(ICluster cluster) + protected void registerOutboundFilter(ICluster cluster) { MessagingService.instance().outboundSink.add((message, to) -> { if (isShutdown()) @@ -372,7 +382,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance int fromNum = config.num(); // since this instance is sending the message, from will always be this instance IInstance toInstance = cluster.get(fromCassandraInetAddressAndPort(to)); if (toInstance == null) - return false; // TODO: Simulator needs this to trigger a failure + return true; // TODO: Simulator needs this to trigger a failure int toNum = toInstance.config().num(); return cluster.filters().permitOutbound(fromNum, toNum, serialzied); }); @@ -429,7 +439,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance byte[] bytes = out.toByteArray(); if (messageOut.serializedSize(toVersion) != bytes.length) throw new AssertionError(String.format("Message serializedSize(%s) does not match what was written with serialize(out, %s) for verb %s and serializer %s; " + - "expected %s, actual %s", toVersion, toVersion, messageOut.verb(), Message.serializer.getClass(), + "expected %s, actual %s ", toVersion, toVersion, messageOut.verb(), Message.serializer.getClass(), messageOut.serializedSize(toVersion), bytes.length)); return new MessageImpl(messageOut.verb().id, bytes, messageOut.id(), toVersion, messageOut.expiresAtNanos(), fromCassandraInetAddressAndPort(from)); } @@ -583,177 +593,28 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance // Otherwise, the instance classloader's logging classes are setup ahead of time and // the patterns/file paths are not set correctly. This will be addressed in a subsequent // commit to extend the functionality of the @Shared annotation to app classes. - assert startedAt.compareAndSet(0L, System.nanoTime()) : "startedAt uninitialized"; + assert startedAt.compareAndSet(0L, System.nanoTime()) : String.format("startedAt on instance %d expected to be 0, but was %d", config().num(), startedAt.get()); sync(() -> { inInstancelogger = LoggerFactory.getLogger(Instance.class); try { - // org.apache.cassandra.distributed.impl.AbstractCluster.startup sets the exception handler for the thread - // so extract it to populate ExecutorFactory.Global - ExecutorFactory.Global.tryUnsafeSet(new ExecutorFactory.Default(Thread.currentThread().getContextClassLoader(), null, Thread.getDefaultUncaughtExceptionHandler())); - if (config.has(GOSSIP)) + boolean isFullStartup = config.get(Constants.KEY_DTEST_FULL_STARTUP) != null && (boolean) config.get(Constants.KEY_DTEST_FULL_STARTUP); + if (isFullStartup) { - // TODO: hacky - RING_DELAY.setLong(15000); - CONSISTENT_RANGE_MOVEMENT.setBoolean(false); - CONSISTENT_SIMULTANEOUS_MOVES_ALLOW.setBoolean(true); - } - - mkdirs(); - - assert config.networkTopology().contains(config.broadcastAddress()) : String.format("Network topology %s doesn't contain the address %s", - config.networkTopology(), config.broadcastAddress()); - DistributedTestSnitch.assign(config.networkTopology()); - - if (config.has(JMX)) - startJmx(); - - DatabaseDescriptor.daemonInitialization(); - LoggingSupportFactory.getLoggingSupport().onStartup(); - - FileUtils.setFSErrorHandler(new DefaultFSErrorHandler()); - DatabaseDescriptor.createAllDirectories(); - CassandraDaemon.getInstanceForTesting().migrateSystemDataIfNeeded(); - CassandraDaemon.logSystemInfo(inInstancelogger); - CommitLog.instance.start(); - - CassandraDaemon.getInstanceForTesting().runStartupChecks(); - - // We need to persist this as soon as possible after startup checks. - // This should be the first write to SystemKeyspace (CASSANDRA-11742) - SystemKeyspace.persistLocalMetadata(config::hostId); - SystemKeyspaceMigrator41.migrate(); - - // Same order to populate tokenMetadata for the first time, - // see org.apache.cassandra.service.CassandraDaemon.setup - StorageService.instance.populateTokenMetadata(); - - try - { - // load schema from disk - Schema.instance.loadFromDisk(); - } - catch (Exception e) - { - throw e; - } - - // Start up virtual table support - CassandraDaemon.getInstanceForTesting().setupVirtualKeyspaces(); - - // clean up debris in data directories - CassandraDaemon.getInstanceForTesting().scrubDataDirectories(); - - Keyspace.setInitialized(); - - // Replay any CommitLogSegments found on disk - try - { - CommitLog.instance.recoverSegmentsOnDisk(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - - // Re-populate token metadata after commit log recover (new peers might be loaded onto system keyspace #10293) - StorageService.instance.populateTokenMetadata(); - - try - { - PaxosState.maybeRebuildUncommittedState(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - - Verb.HINT_REQ.unsafeSetSerializer(DTestSerializer::new); - - if (config.has(NETWORK)) - { - MessagingService.instance().listen(); + assert config.networkTopology().contains(config.broadcastAddress()) : String.format("Network topology %s doesn't contain the address %s", + config.networkTopology(), config.broadcastAddress()); + DistributedTestSnitch.assign(config.networkTopology()); + CassandraDaemon.getInstanceForTesting().activate(false); + // TODO: filters won't work for the messages dispatched during startup + registerInboundFilter(cluster); + registerOutboundFilter(cluster); } else { - // Even though we don't use MessagingService, access the static SocketFactory - // instance here so that we start the static event loop state -// -- not sure what that means? SocketFactory.instance.getClass(); - registerMockMessaging(cluster); + partialStartup(cluster); } - registerInboundFilter(cluster); - registerOutboundFilter(cluster); - if (!config.has(NETWORK)) - { - propagateMessagingVersions(cluster); // fake messaging needs to know messaging version for filters - } - internodeMessagingStarted = true; - - JVMStabilityInspector.replaceKiller(new InstanceKiller(Instance.this::shutdown)); - - // TODO: this is more than just gossip - StorageService.instance.registerDaemon(CassandraDaemon.getInstanceForTesting()); - if (config.has(GOSSIP)) - { - MigrationCoordinator.setUptimeFn(() -> TimeUnit.NANOSECONDS.toMillis(nanoTime() - startedAt.get())); - try - { - StorageService.instance.initServer(); - } - catch (Exception e) - { - // I am tired of looking up my notes for how to fix this... so why not tell the user? - Throwable cause = com.google.common.base.Throwables.getRootCause(e); - if (cause instanceof BindException && "Can't assign requested address".equals(cause.getMessage())) - throw new RuntimeException("Unable to bind, run the following in a termanl and try again:\nfor subnet in $(seq 0 5); do for id in $(seq 0 5); do sudo ifconfig lo0 alias \"127.0.$subnet.$id\"; done; done;", e); - throw e; - } - StorageService.instance.removeShutdownHook(); - - Gossiper.waitToSettle(); - } - else - { - Schema.instance.startSync(); - Stream peers = cluster.stream().filter(instance -> ((IInstance) instance).isValid()); - SystemKeyspace.setLocalHostId(config.hostId()); - if (config.has(BLANK_GOSSIP)) - peers.forEach(peer -> GossipHelper.statusToBlank((IInvokableInstance) peer).accept(this)); - else if (cluster instanceof Cluster) - peers.forEach(peer -> GossipHelper.statusToNormal((IInvokableInstance) peer).accept(this)); - else - peers.forEach(peer -> GossipHelper.unsafeStatusToNormal(this, (IInstance) peer)); - - StorageService.instance.setUpDistributedSystemKeyspaces(); - StorageService.instance.setNormalModeUnsafe(); - Gossiper.instance.register(StorageService.instance); - StorageService.instance.startSnapshotManager(); - StorageService.instance.completeInitialization(); - } - - // Populate tokenMetadata for the second time, - // see org.apache.cassandra.service.CassandraDaemon.setup - StorageService.instance.populateTokenMetadata(); - - CassandraDaemon.getInstanceForTesting().completeSetup(); - - if (config.has(NATIVE_PROTOCOL)) - { - CassandraDaemon.getInstanceForTesting().initializeClientTransports(); - CassandraDaemon.getInstanceForTesting().start(); - } - - if (!FBUtilities.getBroadcastAddressAndPort().getAddress().equals(broadcastAddress().getAddress()) || - FBUtilities.getBroadcastAddressAndPort().getPort() != broadcastAddress().getPort()) - throw new IllegalStateException(String.format("%s != %s", FBUtilities.getBroadcastAddressAndPort(), broadcastAddress())); - - ActiveRepairService.instance().start(); - StreamManager.instance.start(); - - PaxosState.startAutoRepairs(); - - CassandraDaemon.getInstanceForTesting().completeSetup(); + StorageService.instance.startSnapshotManager(); } catch (Throwable t) { @@ -808,15 +669,179 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance }); } + protected void partialStartup(ICluster cluster) throws IOException, NoSuchFieldException, IllegalAccessException, ExecutionException, InterruptedException, StartupException + { + // org.apache.cassandra.distributed.impl.AbstractCluster.startup sets the exception handler for the thread + // so extract it to populate ExecutorFactory.Global + ExecutorFactory.Global.tryUnsafeSet(new ExecutorFactory.Default(Thread.currentThread().getContextClassLoader(), null, Thread.getDefaultUncaughtExceptionHandler())); + if (config.has(GOSSIP)) + { + // TODO: hacky + RING_DELAY.setLong(15000); + CONSISTENT_RANGE_MOVEMENT.setBoolean(false); + CONSISTENT_SIMULTANEOUS_MOVES_ALLOW.setBoolean(true); + } + + mkdirs(); + + assert config.networkTopology().contains(config.broadcastAddress()) : String.format("Network topology %s doesn't contain the address %s", + config.networkTopology(), config.broadcastAddress()); + DistributedTestSnitch.assign(config.networkTopology()); + if (config.has(JMX)) + startJmx(); + DatabaseDescriptor.daemonInitialization(); + LoggingSupportFactory.getLoggingSupport().onStartup(); + logSystemInfo(inInstancelogger); + + FileUtils.setFSErrorHandler(new DefaultFSErrorHandler()); + DatabaseDescriptor.createAllDirectories(); + CassandraDaemon.getInstanceForTesting().migrateSystemDataIfNeeded(); + + CommitLog.instance.start(); + CassandraDaemon.getInstanceForTesting().runStartupChecks(); + + Keyspace.setInitialized(); // TODO: this seems to be superfluous by now + if (!config.has(NETWORK)) + { + propagateMessagingVersions(cluster); // fake messaging needs to know messaging version for filters + } + internodeMessagingStarted = true; + + CassandraDaemon.disableAutoCompaction(Schema.instance.localKeyspaces().names()); + Startup.initialize(DatabaseDescriptor.getSeeds(), + TestProcessor::new, + () -> { + if (config.has(NETWORK)) + { + try + { + MessagingService.instance().waitUntilListening(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + else + { + // Even though we don't use MessagingService, access the static SocketFactory + // instance here so that we start the static event loop state + registerMockMessaging(cluster); + } + registerInboundFilter(cluster); + registerOutboundFilter(cluster); + }); + CassandraDaemon.disableAutoCompaction(Schema.instance.distributedKeyspaces().names()); + QueryProcessor.registerStatementInvalidatingListener(); + TestChangeListener.register(); + + // We need to persist this as soon as possible after startup checks. + // This should be the first write to SystemKeyspace (CASSANDRA-11742) + SystemKeyspace.persistLocalMetadata(); + + // Start up virtual table support + CassandraDaemon.getInstanceForTesting().setupVirtualKeyspaces(); + + // Replay any CommitLogSegments found on disk + try + { + CommitLog.instance.recoverSegmentsOnDisk(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + // Re-populate token metadata after commit log recover (new peers might be loaded onto system keyspace #10293) + //StorageService.instance.populateTokenMetadata(); + + try + { + PaxosState.maybeRebuildUncommittedState(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + Verb.HINT_REQ.unsafeSetSerializer(DTestSerializer::new); + + JVMStabilityInspector.replaceKiller(new InstanceKiller(Instance.this::shutdown)); + + StorageService.instance.registerDaemon(CassandraDaemon.getInstanceForTesting()); + + if (config.has(GOSSIP)) + { + try + { + StorageService.instance.initServer(); + } + catch (Exception e) + { + // I am tired of looking up my notes for how to fix this... so why not tell the user? + Throwable cause = com.google.common.base.Throwables.getRootCause(e); + if (cause instanceof BindException && "Can't assign requested address".equals(cause.getMessage())) + throw new RuntimeException("Unable to bind, run the following in a termanl and try again:\nfor subnet in $(seq 0 5); do for id in $(seq 0 5); do sudo ifconfig lo0 alias \"127.0.$subnet.$id\"; done; done;", e); + throw e; + } + StorageService.instance.removeShutdownHook(); + } + else + { + Stream peers = cluster.stream().filter(IInstance::isValid); + Schema.instance.saveSystemKeyspace(); + ClusterMetadataService.instance().processor().fetchLogAndWait(); + NodeId self = Register.maybeRegister(); + boolean joinRing = config.get(Constants.KEY_DTEST_JOIN_RING) == null || (boolean) config.get(Constants.KEY_DTEST_JOIN_RING); + if (ClusterMetadata.current().directory.peerState(self) != NodeState.JOINED && joinRing) + { + ClusterMetadataService.instance().commit(new UnsafeJoin(self, + new HashSet<>(BootStrapper.getBootstrapTokens(ClusterMetadata.current(), FBUtilities.getBroadcastAddressAndPort())), + ClusterMetadataService.instance().placementProvider())); + + SystemKeyspace.setBootstrapState(SystemKeyspace.BootstrapState.COMPLETED); + if (config.has(BLANK_GOSSIP)) + peers.forEach(peer -> GossipHelper.statusToBlank((IInvokableInstance) peer).accept(this)); + else if (cluster instanceof Cluster) + peers.forEach(peer -> GossipHelper.statusToNormal((IInvokableInstance) peer).accept(this)); + else + peers.forEach(peer -> GossipHelper.unsafeStatusToNormal(this, (IInstance) peer)); + } + Gossiper.instance.register(StorageService.instance); + StorageService.instance.unsafeSetInitialized(); + } + + CassandraDaemon.getInstanceForTesting().completeSetup(); + CassandraDaemon.enableAutoCompaction(Schema.instance.getKeyspaces()); + + if (config.has(NATIVE_PROTOCOL)) + { + CassandraDaemon.getInstanceForTesting().initializeClientTransports(); + CassandraDaemon.getInstanceForTesting().start(); + } + + if (!FBUtilities.getBroadcastAddressAndPort().getAddress().equals(broadcastAddress().getAddress()) || + FBUtilities.getBroadcastAddressAndPort().getPort() != broadcastAddress().getPort()) + throw new IllegalStateException(String.format("%s != %s", FBUtilities.getBroadcastAddressAndPort(), broadcastAddress())); + + ClusterMetadataService.instance().processor().fetchLogAndWait(); + + + ActiveRepairService.instance().start(); + StreamManager.instance.start(); + PaxosState.startAutoRepairs(); + CassandraDaemon.getInstanceForTesting().completeSetup(); + } + @Override public void postStartup() { sync(() -> - StorageService.instance.doAuthSetup(false) + StorageService.instance.doAuthSetup() ).run(); } - private void mkdirs() + protected void mkdirs() { new File(config.getString("saved_caches_directory")).tryCreateDirectories(); new File(config.getString("hints_directory")).tryCreateDirectories(); @@ -840,12 +865,18 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance } @Override - public Future shutdown(boolean graceful) + public Future shutdown(boolean runOnExitThreads) + { + return shutdown(runOnExitThreads, TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY.getBoolean()); + } + + public Future shutdown(boolean runOnExitThreads, boolean shutdownMessagingGracefully) { - inInstancelogger.info("Shutting down instance {} / {}", config.num(), config.broadcastAddress().getHostString()); Future future = async((ExecutorService executor) -> { Throwable error = null; + CompactionManager.instance.forceShutdown(); + error = parallelRun(error, executor, () -> StorageService.instance.setRpcReady(false), CassandraDaemon.getInstanceForTesting()::destroyClientTransports); @@ -853,22 +884,21 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance if (config.has(GOSSIP) || config.has(NETWORK)) { StorageService.instance.shutdownServer(); + error = parallelRun(error, executor, + () -> Gossiper.instance.stopShutdownAndWait(1L, MINUTES)); } error = parallelRun(error, executor, StorageService.instance::disableAutoCompaction); - while (CompactionManager.instance.hasOngoingOrPendingTasks() && !Thread.currentThread().isInterrupted()) - { - inInstancelogger.info("Waiting for compactions to finish"); - Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); - } // trigger init early or else it could try to init and touch a thread pool that got shutdown HintsService hints = HintsService.instance; - ThrowingRunnable shutdownHints = () -> { + ThrowingRunnable shutdownBatchlogAndHints = () -> { // this is to allow shutdown in the case hints were halted already try { - HintsService.instance.shutdownBlocking(); + // Batchlog manager can submit tasks to hints executor, so shut it down earlier and in sequence + BatchlogManager.instance.shutdownAndWait(1L, MINUTES); + hints.shutdownBlocking(); } catch (IllegalStateException e) { @@ -876,11 +906,9 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance throw e; } }; + error = parallelRun(error, executor, - () -> Gossiper.instance.stopShutdownAndWait(1L, MINUTES), - CompactionManager.instance::forceShutdown, - () -> BatchlogManager.instance.shutdownAndWait(1L, MINUTES), - shutdownHints, + shutdownBatchlogAndHints, () -> CompactionLogger.shutdownNowAndWait(1L, MINUTES), () -> AuthCache.shutdownAllAndWait(1L, MINUTES), () -> Sampler.shutdownNowAndWait(1L, MINUTES), @@ -900,13 +928,15 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance () -> SSTableReader.shutdownBlocking(1L, MINUTES), () -> shutdownAndWait(Collections.singletonList(ActiveRepairService.repairCommandExecutor())), () -> ActiveRepairService.instance().shutdownNowAndWait(1L, MINUTES), - () -> SnapshotManager.shutdownAndWait(1L, MINUTES) + () -> org.apache.cassandra.service.snapshot.SnapshotManager.shutdownAndWait(1L, MINUTES) ); internodeMessagingStarted = false; + error = parallelRun(error, executor, () -> ClusterMetadataService.instance().log().close()); + error = parallelRun(error, executor, () -> ScheduledExecutors.shutdownNowAndWait(1L, MINUTES)); error = parallelRun(error, executor, // can only shutdown message once, so if the test shutsdown an instance, then ignore the failure - (IgnoreThrowingRunnable) () -> MessagingService.instance().shutdown(1L, MINUTES, false, config.has(NETWORK)) + (IgnoreThrowingRunnable) () -> MessagingService.instance().shutdown(1L, MINUTES, shutdownMessagingGracefully, config.has(NETWORK)) ); error = parallelRun(error, executor, () -> { if (config.has(NETWORK)) { try { GlobalEventExecutor.INSTANCE.awaitInactivity(1L, MINUTES); } catch (IllegalStateException ignore) {} } }, @@ -918,7 +948,6 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance // (ex. A Mutation stage thread may attempt to add a mutation to the CommitLog.) error = parallelRun(error, executor, CommitLog.instance::shutdownBlocking); error = parallelRun(error, executor, - () -> PendingRangeCalculatorService.instance.shutdownAndWait(1L, MINUTES), () -> shutdownAndWait(Collections.singletonList(JMXBroadcastExecutor.executor)) ); @@ -931,7 +960,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance // Make sure any shutdown hooks registered for DeleteOnExit are released to prevent // references to the instance class loaders from being held - if (graceful) + if (runOnExitThreads) { PathUtils.runOnExitThreadsAndClear(); } @@ -953,10 +982,29 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance { super.shutdown(); startedAt.set(0L); + //withThreadLeakCheck(); } }); } + private void withThreadLeakCheck() + { + StringBuilder sb = new StringBuilder(); + Set threadSet = Thread.getAllStackTraces().keySet(); + threadSet.stream().filter(t -> t.getContextClassLoader() == classLoader).forEach(t -> { + StringBuilder sblocal = new StringBuilder("\nUnterminated thread detected " + t.getName() + " in group " + t.getThreadGroup().getName()); + if (t instanceof NamedThreadFactory.InspectableFastThreadLocalThread) + { + sblocal.append("\nCreation Stack Trace:"); + for (StackTraceElement stackTraceElement : ((NamedThreadFactory.InspectableFastThreadLocalThread) t).creationTrace) + sblocal.append("\n\t\t\t").append(stackTraceElement); + } + sb.append(sblocal); + }); + String msg = sb.toString(); + if (!msg.isEmpty()) + throw new RuntimeException(msg); + } @Override public int liveMemberCount() { @@ -983,22 +1031,10 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance try (CapturingOutput output = new CapturingOutput(); DTestNodeTool nodetool = new DTestNodeTool(withNotifications, output.delegate)) { + SecurityManager before = System.getSecurityManager(); // install security manager to get informed about the exit-code - System.setSecurityManager(new SecurityManager() - { - public void checkExit(int status) - { - throw new SystemExitException(status); - } + ClusterUtils.preventSystemExit(); - public void checkPermission(Permission perm) - { - } - - public void checkPermission(Permission perm, Object context) - { - } - }); int rc; try { @@ -1010,7 +1046,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance } finally { - System.setSecurityManager(null); + System.setSecurityManager(before); } return new NodeToolResult(commandAndArgs, rc, new ArrayList<>(nodetool.notifications.notifications), diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Listen.java b/test/distributed/org/apache/cassandra/distributed/impl/Listen.java index e37c4f7306..377a14a1c3 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Listen.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Listen.java @@ -27,13 +27,6 @@ import org.apache.cassandra.schema.SchemaEvent; public class Listen implements IListen { - final Instance instance; - - public Listen(Instance instance) - { - this.instance = instance; - } - public Cancel schema(Runnable onChange) { Consumer consumer = event -> onChange.run(); diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Query.java b/test/distributed/org/apache/cassandra/distributed/impl/Query.java index 823113f857..3d17ebd07b 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Query.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Query.java @@ -101,11 +101,4 @@ public class Query implements IIsolatedExecutor.SerializableCallable { return org.apache.cassandra.db.ConsistencyLevel.fromCode(cl.ordinal()); } - - static final org.apache.cassandra.distributed.api.ConsistencyLevel[] API_CLs = org.apache.cassandra.distributed.api.ConsistencyLevel.values(); - static org.apache.cassandra.distributed.api.ConsistencyLevel fromCassandraCL(org.apache.cassandra.db.ConsistencyLevel cl) - { - return API_CLs[cl.ordinal()]; - } - } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/impl/TestChangeListener.java b/test/distributed/org/apache/cassandra/distributed/impl/TestChangeListener.java new file mode 100644 index 0000000000..a40ee9a96f --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/TestChangeListener.java @@ -0,0 +1,119 @@ +/* + * 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.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.listeners.ChangeListener; +import org.apache.cassandra.utils.concurrent.WaitQueue; + +public class TestChangeListener implements ChangeListener +{ + private static final Logger logger = LoggerFactory.getLogger(TestChangeListener.class); + public static final TestChangeListener instance = new TestChangeListener(); + + public static void register() + { + logger.debug("Registered TestChangeListener"); + ClusterMetadataService.instance().log().addListener(instance); + } + + private final List> preCommitPredicates = new ArrayList<>(); + private final List> postCommitPredicates = new ArrayList<>(); + private final WaitQueue waiters = WaitQueue.newWaitQueue(); + + @Override + public void notifyPreCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + Iterator> iter = preCommitPredicates.iterator(); + while (iter.hasNext()) + { + if (iter.next().test(next.epoch)) + { + logger.debug("Epoch matches pre-commit predicate, pausing"); + pause(); + iter.remove(); + } + } + } + + @Override + public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot) + { + Iterator> iter = postCommitPredicates.iterator(); + while (iter.hasNext()) + { + if (iter.next().test(next.epoch)) + { + logger.debug("Epoch matches post-commit predicate, pausing"); + pause(); + iter.remove(); + } + } + } + + public void pauseBefore(Epoch epoch, Runnable onMatch) + { + logger.debug("Requesting pause before enacting {}", epoch); + preCommitPredicates.add((e) -> { + if (e.is(epoch)) + { + onMatch.run(); + return true; + } + return false; + }); + } + + public void pauseAfter(Epoch epoch, Runnable onMatch) + { + logger.debug("Requesting pause after enacting {}", epoch); + postCommitPredicates.add((e) -> { + if (e.is(epoch)) + { + onMatch.run(); + return true; + } + return false; + }); + } + + public void pause() + { + WaitQueue.Signal signal = waiters.register(); + logger.debug("Log follower is paused, waiting..."); + signal.awaitUninterruptibly(); + logger.debug("Resumed log follower..."); + } + + public void unpause() + { + logger.debug("Unpausing log follower"); + waiters.signalAll(); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/UnsafeGossipHelper.java b/test/distributed/org/apache/cassandra/distributed/impl/UnsafeGossipHelper.java index 7cf4c167f7..c111946bed 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/UnsafeGossipHelper.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/UnsafeGossipHelper.java @@ -36,8 +36,8 @@ import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import static com.google.common.collect.Iterables.getOnlyElement; @@ -107,7 +107,7 @@ public class UnsafeGossipHelper } SystemKeyspace.setLocalHostId(hostId); - SystemKeyspace.updateTokens(singleton(token)); + SystemKeyspace.updateLocalTokens(singleton(token)); } else { @@ -137,7 +137,6 @@ public class UnsafeGossipHelper : Math.min(MessagingService.current_version, messagingVersion); MessagingService.instance().versions.set(addressAndPort, setMessagingVersion); - PendingRangeCalculatorService.instance.blockUntilFinished(); } catch (Throwable e) // UnknownHostException { @@ -201,7 +200,6 @@ public class UnsafeGossipHelper Gossiper.instance.initializeNodeUnsafe(addressAndPort, hostId, 1); Gossiper.instance.realMarkAlive(addressAndPort, Gossiper.instance.getEndpointStateForEndpoint(addressAndPort)); }); - PendingRangeCalculatorService.instance.blockUntilFinished(); } catch (Throwable e) // UnknownHostException { @@ -253,7 +251,7 @@ public class UnsafeGossipHelper public static void addToRingNormal(IInstance peer) { addToRingNormalRunner(peer).run(); - assert StorageService.instance.getTokenMetadata().isMember(toCassandraInetAddressAndPort(peer.broadcastAddress())); + assert ClusterMetadata.current().directory.allAddresses().contains(toCassandraInetAddressAndPort(peer.broadcastAddress())); } public static void addToRingBootstrapping(IInstance peer) diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java index 4de180636c..a1a29f09cb 100644 --- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java +++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java @@ -45,6 +45,7 @@ import org.apache.cassandra.service.GCInspector; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.StreamManager; +import org.apache.cassandra.tcm.CMSOperations; import org.apache.cassandra.tools.NodeProbe; public class InternalNodeProbe extends NodeProbe @@ -68,6 +69,7 @@ public class InternalNodeProbe extends NodeProbe StorageService.instance.skipNotificationListeners = !withNotifications; ssProxy = StorageService.instance; + cmsProxy = CMSOperations.instance; msProxy = MessagingService.instance(); streamProxy = StreamManager.instance; compactionProxy = CompactionManager.instance; diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java index 5a8da8c7cf..ac8aea6591 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java @@ -18,9 +18,9 @@ package org.apache.cassandra.distributed.shared; +import java.io.Serializable; import java.lang.reflect.Field; import java.net.InetSocketAddress; -import java.security.Permission; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -30,24 +30,29 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; -import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.io.util.File; import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import harry.core.VisibleForTesting; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; @@ -56,16 +61,35 @@ import org.apache.cassandra.distributed.api.IMessageFilters; import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.impl.AbstractCluster; import org.apache.cassandra.distributed.impl.InstanceConfig; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.distributed.impl.TestChangeListener; +import org.apache.cassandra.distributed.test.log.TestProcessor; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.tools.SystemExitException; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementForRange; import org.apache.cassandra.utils.Isolated; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.CountDownLatch; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SCHEMA_DELAY_MS; import static org.apache.cassandra.config.CassandraRelevantProperties.BROADCAST_INTERVAL_MS; import static org.apache.cassandra.config.CassandraRelevantProperties.REPLACE_ADDRESS_FIRST_BOOT; import static org.apache.cassandra.config.CassandraRelevantProperties.RING_DELAY; +import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.toCassandraInetAddressAndPort; import static org.assertj.core.api.Assertions.assertThat; /** @@ -79,6 +103,7 @@ import static org.assertj.core.api.Assertions.assertThat; @Isolated public class ClusterUtils { + private static final Logger logger = LoggerFactory.getLogger(ClusterUtils.class); /** * Start the instance with the given System Properties, after the instance has started, the properties will be cleared. */ @@ -215,7 +240,7 @@ public class ClusterUtils * @param instance type * @return the instance added */ - public static I replaceHostAndStart(AbstractCluster cluster, IInstance toReplace) + public static I replaceHostAndStart(AbstractCluster cluster, I toReplace) { return replaceHostAndStart(cluster, toReplace, ignore -> {}); } @@ -232,7 +257,7 @@ public class ClusterUtils * @return the instance added */ public static I replaceHostAndStart(AbstractCluster cluster, - IInstance toReplace, + I toReplace, Consumer fn) { return replaceHostAndStart(cluster, toReplace, (ignore, prop) -> fn.accept(prop)); @@ -250,12 +275,27 @@ public class ClusterUtils * @return the instance added */ public static I replaceHostAndStart(AbstractCluster cluster, - IInstance toReplace, + I toReplace, BiConsumer fn) { IInstanceConfig toReplaceConf = toReplace.config(); - I inst = addInstance(cluster, toReplaceConf, c -> c.set("auto_bootstrap", true)); + I inst = addInstance(cluster, toReplaceConf, c -> c.set("auto_bootstrap", true) + .set("progress_barrier_min_consistency_level", ConsistencyLevel.ONE)); + return startHostReplacement(toReplace, inst, fn); + } + + /** + * Start a instance with the properties needed to perform a host replacement. + * + * @param toReplace instance to replace + * @param inst to start + * @param fn lambda to add additional properties or modify instance + * @param instance type + * @return inst + */ + public static I startHostReplacement(I toReplace, I inst, BiConsumer fn) + { return start(inst, properties -> { // lower this so the replacement waits less time properties.set(BROADCAST_INTERVAL_MS, Long.toString(TimeUnit.SECONDS.toMillis(30))); @@ -271,27 +311,96 @@ public class ClusterUtils } /** - * Calls {@link org.apache.cassandra.locator.TokenMetadata#sortedTokens()}, returning as a list of strings. + * Calls TokenMap#tokens(), returning as a list of strings. */ public static List getTokenMetadataTokens(IInvokableInstance inst) { return inst.callOnInstance(() -> - StorageService.instance.getTokenMetadata() - .sortedTokens().stream() - .map(Object::toString) - .collect(Collectors.toList())); + ClusterMetadata.current().tokenMap.tokens() + .stream() + .map(Object::toString) + .collect(Collectors.toList())); } public static Collection getLocalTokens(IInvokableInstance inst) { return inst.callOnInstance(() -> { List tokens = new ArrayList<>(); - for (Token t : StorageService.instance.getTokenMetadata().getTokens(FBUtilities.getBroadcastAddressAndPort())) + + for (Token t : ClusterMetadata.current().tokenMap.tokens(ClusterMetadata.current().myNodeId())) tokens.add(t.getTokenValue().toString()); return tokens; }); } + public static List getPeerDirectoryDebugStrings(IInvokableInstance inst) + { + String s = inst.callOnInstance(() -> ClusterMetadata.current().directory.toDebugString()); + return Arrays.asList(s.split("\n")); + } + + public static List getTokenMapDebugStrings(IInvokableInstance inst) + { + String s = inst.callOnInstance(() -> ClusterMetadata.current().tokenMap.toDebugString()); + return Arrays.asList(s.split("\n")); + } + + public static void logTokenMapDebugString(IInvokableInstance inst) + { + inst.runOnInstance(() -> ClusterMetadata.current().tokenMap.logDebugString()); + } + + @SuppressWarnings("rawtypes") + public static Map getDataPlacementDebugInfo(IInvokableInstance inst) + { + return inst.callOnInstance(() -> getPlacementDebugInfo(ClusterMetadataService.instance())); + } + + + // not pretty, but this is for testing. For each keyspace, includes 2—element array of List. + // Element 0 is the read replicas for the keyspace, element 1 is the write replicas. + @VisibleForTesting + @SuppressWarnings("rawtypes") + public static Map getPlacementDebugInfo(ClusterMetadataService metadataService) + { + ClusterMetadata metadata = metadataService.metadata(); + Map byKeyspace = new HashMap<>(); + for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces()) + { + List[] placements = new List[2]; + placements[0] = metadata.placements.get(keyspace.params.replication).reads.toReplicaStringList(); + placements[1] = metadata.placements.get(keyspace.params.replication).writes.toReplicaStringList(); + byKeyspace.put(keyspace.name, placements); + } + return byKeyspace; + } + + public static void logDataPlacementDebugString(IInvokableInstance inst, boolean byEndpoint) + { + inst.runOnInstance(() -> logPlacementDebugString(ClusterMetadataService.instance(), byEndpoint)); + } + + public static void logPlacementDebugString(ClusterMetadataService metadataService, boolean byEndpoint) + { + ClusterMetadata metadata = metadataService.metadata(); + List keyspaces = new ArrayList<>(); + for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces()) + { + StringBuilder builder = new StringBuilder(); + builder.append("'keyspace' { 'name':").append(keyspace.name).append("', "); + builder.append("'reads':['"); + PlacementForRange placement = metadata.placements.get(keyspace.params.replication).reads; + builder.append(byEndpoint ? placement.toStringByEndpoint() : placement.toString()); + builder.append("'], 'writes':['"); + placement = metadata.placements.get(keyspace.params.replication).writes; + builder.append(byEndpoint ? placement.toStringByEndpoint() : placement.toString()); + builder.append("']}"); + keyspaces.add(builder.toString()); + } + String debug = String.join("\n", keyspaces); + logger.debug(debug); + } + public static void runAndWaitForLogs(Runnable r, String waitString, AbstractCluster cluster) throws TimeoutException { runAndWaitForLogs(r, waitString, cluster.stream().toArray(IInstance[]::new)); @@ -307,6 +416,312 @@ public class ClusterUtils instances[i].logs().watchFor(marks[i], waitString); } + public static Epoch getClusterMetadataVersion(IInvokableInstance inst) + { + return decode(inst.callOnInstance(() -> encode(ClusterMetadata.current().epoch))); + } + + public static long encode(Epoch epoch) + { + return epoch.getEpoch(); + } + + public static Epoch decode(long periodEpoch) + { + return Epoch.create(periodEpoch); + } + + public static void waitForCMSToQuiesce(ICluster cluster, IInvokableInstance leader, int...ignored) + { + ClusterUtils.waitForCMSToQuiesce(cluster, getClusterMetadataVersion(leader), ignored); + } + + public static Callable pauseBeforeEnacting(IInvokableInstance instance, Epoch epoch) + { + return pauseBeforeEnacting(instance, epoch, 10, TimeUnit.SECONDS); + } + + protected static Callable pauseBeforeEnacting(IInvokableInstance instance, + Epoch epoch, + long wait, + TimeUnit waitUnit) + { + return instance.callOnInstance(() -> { + TestChangeListener listener = TestChangeListener.instance; + AsyncPromise promise = new AsyncPromise<>(); + listener.pauseBefore(epoch, () -> promise.setSuccess(null)); + return () -> { + try + { + promise.get(wait, waitUnit); + return null; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }; + }); + } + + public static Callable pauseAfterEnacting(IInvokableInstance instance, Epoch epoch) + { + return pauseAfterEnacting(instance, epoch, 10, TimeUnit.SECONDS); + } + + protected static Callable pauseAfterEnacting(IInvokableInstance instance, + Epoch epoch, + long wait, + TimeUnit waitUnit) + { + return instance.callOnInstance(() -> { + TestChangeListener listener = TestChangeListener.instance; + AsyncPromise promise = new AsyncPromise<>(); + listener.pauseAfter(epoch, () -> promise.setSuccess(null)); + return () -> { + try + { + promise.get(wait, waitUnit); + return null; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }; + }); + } + + public static Callable pauseBeforeCommit(IInvokableInstance cmsInstance, SerializablePredicate predicate) + { + Callable remoteCallable = cmsInstance.callOnInstance(() -> { + TestProcessor processor = (TestProcessor) ((ClusterMetadataService.SwitchableProcessor) ClusterMetadataService.instance().processor()).delegate(); + AsyncPromise promise = new AsyncPromise<>(); + processor.pauseIf(predicate, () -> promise.setSuccess(ClusterMetadata.current().epoch)); + return () -> { + try + { + return promise.get(30, TimeUnit.SECONDS).getEpoch(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }; + }); + return () -> Epoch.create(remoteCallable.call()); + + } + + public static Callable getSequenceAfterCommit(IInvokableInstance cmsInstance, + SerializableBiPredicate predicate) + { + Callable remoteCallable = cmsInstance.callOnInstance(() -> { + TestProcessor processor = (TestProcessor) ((ClusterMetadataService.SwitchableProcessor) ClusterMetadataService.instance().processor()).delegate(); + + AsyncPromise promise = new AsyncPromise<>(); + processor.registerCommitPredicate((event, result) -> { + if (predicate.test(event, result)) + { + promise.setSuccess(result.success().replication.latestEpoch()); + return true; + } + + return false; + }); + return () -> { + try + { + return promise.get(30, TimeUnit.SECONDS).getEpoch(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }; + }); + + return () -> Epoch.create(remoteCallable.call()); + } + + public static void unpauseCommits(IInvokableInstance instance) + { + instance.runOnInstance(() -> { + TestProcessor processor = (TestProcessor) ((ClusterMetadataService.SwitchableProcessor) ClusterMetadataService.instance().processor()).delegate(); + processor.unpause(); + }); + } + + public static void unpauseEnactment(IInvokableInstance instance) + { + instance.runOnInstance(() -> TestChangeListener.instance.unpause()); + } + + public static boolean isMigrating(IInvokableInstance instance) + { + return instance.callOnInstance(() -> ClusterMetadataService.instance().isMigrating()); + } + + public static interface SerializablePredicate extends Predicate, Serializable + {} + + public static interface SerializableBiPredicate extends BiPredicate, Serializable {} + + private static class ClusterMetadataVersion + { + public final int node; + public final Epoch epoch; + public final Epoch replicated; + + private ClusterMetadataVersion(int node, Epoch epoch, Epoch replicated) + { + this.node = node; + this.epoch = epoch; + this.replicated = replicated; + } + + public String toString() + { + return "Version{" + + "node=" + node + + ", epoch=" + epoch + + ", replicated=" + replicated + + '}'; + } + } + + public static void waitForCMSToQuiesce(ICluster cluster, Epoch awaitedEpoch, int...ignored) + { + List notMatching = new ArrayList<>(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); + while (System.nanoTime() < deadline) + { + notMatching.clear(); + for (int j = 1; j <= cluster.size(); j++) + { + boolean skip = false; + for (int ignore : ignored) + if (ignore == j) + skip = true; + + if (skip) + continue; + + if (cluster.get(j).isShutdown()) + continue; + Epoch version = getClusterMetadataVersion(cluster.get(j)); + if (!awaitedEpoch.equals(version)) + notMatching.add(new ClusterMetadataVersion(j, version, getClusterMetadataVersion(cluster.get(j)))); + } + if (notMatching.isEmpty()) + return; + + sleepUninterruptibly(10, TimeUnit.MILLISECONDS); + } + throw new AssertionError(String.format("Some instances have not reached schema agreement with the leader. Awaited %s; diverging nodes: %s. ", awaitedEpoch, notMatching)); + } + + public static Epoch getCurrentEpoch(IInvokableInstance inst) + { + return decode(inst.callOnInstance(() -> encode(ClusterMetadata.current().epoch))); + } + + public static Epoch getNextEpoch(IInvokableInstance inst) + { + return decode(inst.callOnInstance(() -> encode(ClusterMetadata.current().nextEpoch()))); + } + + public static Map getPeerEpochs(IInvokableInstance requester) + { + Map map = requester.callOnInstance(() -> { + ImmutableList peers = ClusterMetadata.current().directory.allAddresses(); + CountDownLatch latch = CountDownLatch.newCountDownLatch(peers.size()); + Map epochs = new ConcurrentHashMap<>(peers.size()); + peers.forEach(peer -> { + Message request = Message.out(Verb.TCM_CURRENT_EPOCH_REQ, ClusterMetadata.current().epoch); + RequestCallback callback = response -> { + epochs.put(peer.toString(), encode(response.payload)); + latch.decrement(); + }; + MessagingService.instance().sendWithCallback(request, peer, callback); + }); + latch.awaitUninterruptibly(); + return epochs; + }); + return map.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> decode(e.getValue()))); + } + + public static Set getCMSMembers(IInvokableInstance inst) + { + return inst.callOnInstance(() -> ClusterMetadata.current() + .fullCMSMembers() + .stream() + .map(InetSocketAddress::getAddress) + .map(Object::toString) + .collect(Collectors.toSet())); + } + + public static boolean decommission(IInvokableInstance leaving) + { + return leaving.callOnInstance(() -> { + try + { + StorageService.instance.decommission(true); + return true; + } + catch (Exception e) + { + e.printStackTrace(); + return false; + } + }); + } + + public static NodeId getNodeId(IInvokableInstance target) + { + return new NodeId(getNodeId(target, target)); + } + + public static int getNodeId(IInvokableInstance target, IInvokableInstance executor) + { + InetSocketAddress targetAddress = target.config().broadcastAddress(); + return executor.callOnInstance(() -> { + try + { + return ClusterMetadata.current().directory.peerId(toCassandraInetAddressAndPort(targetAddress)).id(); + } + catch (Exception e) + { + e.printStackTrace(); + return null; + } + }); + } + + public static boolean cancelInProgressSequences(IInvokableInstance executor) + { + return cancelInProgressSequences(getNodeId(executor), executor); + } + + public static boolean cancelInProgressSequences(NodeId nodeId, IInvokableInstance executor) + { + int id = nodeId.id(); + return executor.callOnInstance(() -> { + try + { + + StorageService.instance.cancelInProgressSequences(new NodeId(id)); + return true; + } + catch (Exception e) + { + e.printStackTrace(); + return false; + } + }); + } /** * Get the ring from the perspective of the instance. @@ -958,23 +1373,7 @@ public class ClusterUtils public static void preventSystemExit() { - System.setSecurityManager(new SecurityManager() - { - @Override - public void checkExit(int status) - { - throw new SystemExitException(status); - } - - @Override - public void checkPermission(Permission perm) - { - } - - @Override - public void checkPermission(Permission perm, Object context) - { - } - }); + System.setSecurityManager(new PreventSystemExit()); } } + diff --git a/test/distributed/org/apache/cassandra/distributed/shared/PreventSystemExit.java b/test/distributed/org/apache/cassandra/distributed/shared/PreventSystemExit.java new file mode 100644 index 0000000000..9e1bd4d43e --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/shared/PreventSystemExit.java @@ -0,0 +1,44 @@ +/* + * 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.shared; + +import java.security.Permission; + +import org.apache.cassandra.tools.SystemExitException; +import org.apache.cassandra.utils.Shared; + +@Shared +public class PreventSystemExit extends SecurityManager +{ + @Override + public void checkExit(int status) + { + throw new SystemExitException(status); + } + + @Override + public void checkPermission(Permission perm) + { + } + + @Override + public void checkPermission(Permission perm, Object context) + { + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java b/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java index bc7159b928..553f9d26f8 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AlterTest.java @@ -23,7 +23,6 @@ import java.util.List; import com.google.common.collect.ImmutableMap; import org.junit.Assert; import org.junit.Test; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +37,6 @@ import org.apache.cassandra.distributed.api.IIsolatedExecutor; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.shared.ClusterUtils; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.Throwables; import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; @@ -100,16 +98,6 @@ public class AlterTest extends TestBaseImpl IInstanceConfig config = cluster.newInstanceConfig(); IInvokableInstance gossippingOnlyMember = cluster.bootstrap(config); withProperty(JOIN_RING, false, () -> gossippingOnlyMember.startup(cluster)); - - int attempts = 0; - // it takes some time the underlying structure is populated otherwise the test is flaky - while (((IInvokableInstance) (cluster.get(2))).callOnInstance(() -> StorageService.instance.getTokenMetadata().getAllMembers().isEmpty())) - { - if (attempts++ > 30) - throw new RuntimeException("timeouted on waiting for a member"); - Thread.sleep(1000); - } - for (String operation : new String[] { "CREATE", "ALTER" }) { SimpleQueryResult result = cluster.coordinator(2) diff --git a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java index 67cf4c79fb..8f67a6ac79 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AuthTest.java @@ -20,16 +20,14 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.junit.Test; import com.datastax.driver.core.PlainTextAuthProvider; -import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy; +import org.apache.cassandra.auth.CassandraRoleManager; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.ICoordinator; @@ -37,8 +35,8 @@ import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IMessageFilters.Filter; import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.util.Auth; import org.apache.cassandra.locator.SimpleSeedProvider; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.StorageService; import static java.util.concurrent.TimeUnit.SECONDS; @@ -70,7 +68,9 @@ public class AuthTest extends TestBaseImpl } /** - * See CASSANDRA-12525 for more information. + * CASSANDRA-12525 has solved this issue in a way that was reconciling the passwords (ie any override of the password would + * supercede default password as soon as node learns about existence of other peers). With transactional metadata, this + * issue simply does not exist since nodes always know about the auth placements. */ @Test public void testZeroTimestampForDefaultRoleCreation() throws Exception @@ -79,10 +79,11 @@ public class AuthTest extends TestBaseImpl .withNodes(1) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(2, 1)) .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL) - .set("authenticator", "PasswordAuthenticator")) + .set("authenticator", "PasswordAuthenticator") + .set("credentials_validity", "2s")) // revert to OSS default .start()) { - Auth.waitForExistingRoles(cluster.get(1)); + waitForExistingRoles(cluster.get(1)); long writeTime = getPasswordWritetime(cluster.coordinator(1)); // TIMESTAMP 0 in action @@ -97,42 +98,38 @@ public class AuthTest extends TestBaseImpl IInvokableInstance secondNode = getSecondNode(cluster); // drop all communication between nodes - Filter to = cluster.filters().allVerbs().inbound().drop(); - Filter from = cluster.filters().allVerbs().outbound().drop(); + Filter to = cluster.filters().inbound() + .messagesMatching((i, i1, msg) -> !Verb.fromId(msg.verb()).toString().contains("TCM")) + .drop(); + Filter from = cluster.filters().outbound() + .messagesMatching((i, i1, msg) -> !Verb.fromId(msg.verb()).toString().contains("TCM")) + .drop(); secondNode.startup(); - Auth.waitForExistingRoles(secondNode); - - long passwordWritetimeOnSecondNode = getPasswordWritetime(cluster.coordinator(2)); - - // as new node thinks it is alone in cluster, it created new role with TIMESTAMP 0 - assertEquals(0, passwordWritetimeOnSecondNode); - - // the fact we can still log in with old password shows we dropped all communication - // and the second node thinks that it is alone in the cluster, so it created new cassandra role - // with default password - doWithSession("127.0.0.2", - "datacenter2", - "cassandra", session -> session.execute("select * from system.local")); // turn off filters to.off(); from.off(); - // be sure the first peer is there for the second node - await() - .atMost(1, TimeUnit.MINUTES) - .pollInterval(10, SECONDS) - .until(() -> { - List rows = doWithSession("127.0.0.2", - "datacenter2", - "cassandra", - session -> session.execute("select * from system.peers")).all(); - if (rows.isEmpty()) - return false; + try + { + waitForExistingRoles(secondNode); + } + catch (Throwable t) + { + assertTrue(t.getMessage().contains("ReadTimeoutException")); + } - return rows.get(0).getInet("peer").getHostAddress().equals("127.0.0.1"); - }); + // Node has started with auto_bootstrap=false, and it just so happens that this key belongs to node2, so we get no results + assertEquals(0L, + cluster.coordinator(2) + .execute("SELECT WRITETIME (salted_hash) from system_auth.roles where role = 'cassandra'", + ConsistencyLevel.LOCAL_ONE)[0][0]); + + // since Auth is initialized once per cluster now, we naturally can _not_ authenticate because the keyspace is + doWithSession("127.0.0.2", + "datacenter2", + "cassandra", session -> session.execute("select * from system.local")); // change the replication strategy doWithSession("127.0.0.2", @@ -164,6 +161,14 @@ public class AuthTest extends TestBaseImpl return cluster.bootstrap(config); } + private void waitForExistingRoles(IInvokableInstance instance) + { + await().pollDelay(1, SECONDS) + .pollInterval(1, SECONDS) + .atMost(30, SECONDS) + .until(() -> instance.callOnInstance(CassandraRoleManager::hasExistingRoles)); + } + private long getPasswordWritetime(ICoordinator coordinator) { return (Long) coordinator.execute("SELECT WRITETIME (salted_hash) from system_auth.roles where role = 'cassandra'", diff --git a/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java b/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java index d6abd35788..243ef022a2 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ByteBuddyExamplesTest.java @@ -20,6 +20,7 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -54,6 +55,7 @@ public class ByteBuddyExamplesTest extends TestBaseImpl .start())) { cluster.schemaChange("create table " + KEYSPACE + ".tbl (id int primary key, t int)"); + cluster.get(1).runOnInstance(() -> BBFailHelper.enabled.set(true)); try { cluster.coordinator(1).execute("insert into " + KEYSPACE + ".tbl (id, t) values (1, 1)", ConsistencyLevel.ALL); @@ -70,15 +72,21 @@ public class ByteBuddyExamplesTest extends TestBaseImpl { static void install(ClassLoader cl, int nodeNumber) { - new ByteBuddy().redefine(ModificationStatement.class) + new ByteBuddy().rebase(ModificationStatement.class) .method(named("execute")) .intercept(MethodDelegation.to(BBFailHelper.class)) .make() .load(cl, ClassLoadingStrategy.Default.INJECTION); } - public static ResultMessage execute() + + public static AtomicBoolean enabled = new AtomicBoolean(false); + + public static ResultMessage execute(QueryState v1, QueryOptions v2, long v3, @SuperCall Callable zuper) throws Exception { - throw new RuntimeException(); + if (enabled.get()) + throw new RuntimeException(); + + return zuper.call(); } } @@ -90,6 +98,7 @@ public class ByteBuddyExamplesTest extends TestBaseImpl .start())) { cluster.schemaChange("create table " + KEYSPACE + ".tbl (id int primary key, bytebuddy_test_column int)"); + cluster.get(1).runOnInstance(() -> BBCountHelper.count.set(0)); cluster.coordinator(1).execute("select * from " + KEYSPACE + ".tbl;", ConsistencyLevel.ALL); cluster.coordinator(2).execute("select * from " + KEYSPACE + ".tbl;", ConsistencyLevel.ALL); cluster.get(1).runOnInstance(() -> { diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java b/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java index d6c892e151..50ddd09158 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java @@ -19,9 +19,8 @@ package org.apache.cassandra.distributed.test; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.exceptions.InvalidRequestException; @@ -33,17 +32,10 @@ import static org.apache.cassandra.distributed.shared.AssertUtils.row; public class CASAddTest extends TestBaseImpl { - private static final Logger logger = LoggerFactory.getLogger(CASAddTest.class); - - /** - * The {@code cas_contention_timeout} used during the tests - */ - private static final long CONTENTION_TIMEOUT = 1000L; - - /** - * The {@code write_request_timeout} used during the tests - */ - private static final long REQUEST_TIMEOUT = 1000L; + static + { + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + } @Test public void testAddition() throws Throwable diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASContentionTest.java b/test/distributed/org/apache/cassandra/distributed/test/CASContentionTest.java index 60e8db0cb1..c58a9458c9 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CASContentionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CASContentionTest.java @@ -20,6 +20,7 @@ package org.apache.cassandra.distributed.test; import com.google.common.util.concurrent.Uninterruptibles; import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.service.paxos.ContentionStrategy; @@ -43,6 +44,11 @@ public class CASContentionTest extends CASTestBase { private static Cluster THREE_NODES; + static + { + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + } + @BeforeClass public static void beforeClass() throws Throwable { diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASMultiDCTest.java b/test/distributed/org/apache/cassandra/distributed/test/CASMultiDCTest.java index e36f34b837..382cb543c7 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CASMultiDCTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CASMultiDCTest.java @@ -26,6 +26,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.distributed.Cluster; @@ -39,6 +40,11 @@ import static org.apache.cassandra.distributed.api.ConsistencyLevel.*; public class CASMultiDCTest { + static + { + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + } + private static Cluster CLUSTER; private static final String KEYSPACE = "ks"; private static final String TABLE = "tbl"; diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASTest.java b/test/distributed/org/apache/cassandra/distributed/test/CASTest.java index dc792564ff..e5b11085f3 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CASTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CASTest.java @@ -30,6 +30,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.ICoordinator; @@ -61,6 +62,11 @@ import static org.junit.Assert.fail; public class CASTest extends CASCommonTestCases { + static + { + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + } + /** * The {@code cas_contention_timeout} used during the tests */ diff --git a/test/distributed/org/apache/cassandra/distributed/test/CASTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/CASTestBase.java index cae6eefef8..d0819d1930 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CASTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CASTestBase.java @@ -45,9 +45,9 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.paxos.PaxosRepair; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -160,22 +160,21 @@ public abstract class CASTestBase extends TestBaseImpl UUID hostId = config.hostId(); Gossiper.runInGossipStageBlocking(() -> { Gossiper.instance.initializeNodeUnsafe(address, hostId, 1); - Gossiper.instance.injectApplicationState(address, - ApplicationState.TOKENS, - new VersionedValue.VersionedValueFactory(partitioner).tokens(Collections.singleton(token))); - VersionedValue status = bootstrapping - ? new VersionedValue.VersionedValueFactory(partitioner).bootstrapping(Collections.singleton(token)) - : new VersionedValue.VersionedValueFactory(partitioner).normal(Collections.singleton(token)); - Gossiper.instance.injectApplicationState(address, ApplicationState.STATUS, status); - StorageService.instance.onChange(address, ApplicationState.STATUS, status); + Gossiper.instance.injectApplicationState(address, + ApplicationState.TOKENS, + new VersionedValue.VersionedValueFactory(partitioner).tokens(Collections.singleton(token))); + VersionedValue status = bootstrapping + ? new VersionedValue.VersionedValueFactory(partitioner).bootstrapping(Collections.singleton(token)) + : new VersionedValue.VersionedValueFactory(partitioner).normal(Collections.singleton(token)); + Gossiper.instance.injectApplicationState(address, ApplicationState.STATUS, status); + StorageService.instance.onChange(address, ApplicationState.STATUS, status); Gossiper.instance.realMarkAlive(address, Gossiper.instance.getEndpointStateForEndpoint(address)); }); int version = Math.min(MessagingService.current_version, peer.getMessagingVersion()); MessagingService.instance().versions.set(address, version); if (!bootstrapping) - assert StorageService.instance.getTokenMetadata().isMember(address); - PendingRangeCalculatorService.instance.blockUntilFinished(); + assert ClusterMetadata.current().directory.allAddresses().contains(address); } catch (Throwable e) // UnknownHostException { @@ -206,7 +205,6 @@ public abstract class CASTestBase extends TestBaseImpl Gossiper.instance.unsafeAnnulEndpoint(address); Gossiper.instance.realMarkAlive(address, new EndpointState(new HeartBeatState(0, 0))); }); - PendingRangeCalculatorService.instance.blockUntilFinished(); } catch (Throwable e) // UnknownHostException { @@ -223,7 +221,7 @@ public abstract class CASTestBase extends TestBaseImpl public static void addToRingNormal(IInstance peer) { addToRing(false, peer); - assert StorageService.instance.getTokenMetadata().isMember(InetAddressAndPort.getByAddress(peer.broadcastAddress())); + assert ClusterMetadata.current().directory.allAddresses().contains(InetAddressAndPort.getByAddress(peer.broadcastAddress())); } public static void addToRingBootstrapping(IInstance peer) diff --git a/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java index 099f87dd40..6cc0fdfe49 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CompactionDiskSpaceTest.java @@ -58,7 +58,7 @@ public class CompactionDiskSpaceTest extends TestBaseImpl cluster.schemaChange("create table "+KEYSPACE+".tbl (id int primary key, x int) with compaction = {'class':'SizeTieredCompactionStrategy'}"); cluster.coordinator(1).execute("insert into "+KEYSPACE+".tbl (id, x) values (1,1)", ConsistencyLevel.ALL); cluster.get(1).flush(KEYSPACE); - cluster.setUncaughtExceptionsFilter((t) -> t.getMessage() != null && t.getMessage().contains("Not enough space for compaction") && t.getMessage().contains(KEYSPACE+".tbl")); + cluster.setUncaughtExceptionsFilter((t) -> t.getMessage() != null && t.getMessage().contains("Not enough space for compaction")); cluster.get(1).runOnInstance(() -> { ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl"); BB.estimatedRemaining.set(2000); diff --git a/test/distributed/org/apache/cassandra/distributed/test/CreateTableNonDeterministicTest.java b/test/distributed/org/apache/cassandra/distributed/test/CreateTableNonDeterministicTest.java new file mode 100644 index 0000000000..74c08047fc --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CreateTableNonDeterministicTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.io.IOException; +import java.util.UUID; + +import org.junit.Test; + +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.tcm.ClusterMetadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + + +public class CreateTableNonDeterministicTest extends TestBaseImpl +{ + @Test + public void test() throws IOException + { + try (Cluster cluster = init(Cluster.build(2).start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + TableId node1id = tableId(cluster.get(1), "tbl"); + TableId node2id = tableId(cluster.get(2), "tbl"); + assertEquals(node1id, node2id); + cluster.schemaChange(withKeyspace("drop table %s.tbl")); + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + TableId node1id2 = tableId(cluster.get(1), "tbl"); + TableId node2id2 = tableId(cluster.get(2), "tbl"); + assertNotEquals(node1id, node1id2); + assertEquals(node1id2, node2id2); + } + } + + @Test + public void testIdClash() throws IOException + { + try (Cluster cluster = init(Cluster.build(2).start())) + { + long epoch = epoch(cluster.get(1)); + for (long i = epoch + 10; i < epoch + 15; i++) + { + cluster.schemaChange(withKeyspace("create table %s.tbl" + i + " (id int primary key) with id = " + new UUID(TableId.MAGIC, i))); + TableId justCreated = tableId(cluster.get(1), "tbl"+i); + assertEquals(justCreated.asUUID().getLeastSignificantBits(), i); + } + + for (int i = 0; i < 10; i++) + { + long epochBeforeCreate = epoch(cluster.get(1)); + cluster.schemaChange(withKeyspace("create table %s.tblx" + i + " (id int primary key)")); + TableId justCreated = tableId(cluster.get(1), "tblx"+i); + long lsb = justCreated.asUUID().getLeastSignificantBits(); + assertEquals(epochBeforeCreate, lsb - (i < 5 ? 0 : 5)); + } + } + } + + long epoch(IInvokableInstance inst) + { + return inst.callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + } + + TableId tableId(IInvokableInstance inst, String tbl) + { + return TableId.fromString(inst.callOnInstance(() -> Keyspace.open(KEYSPACE).getColumnFamilyStore(tbl).metadata.id.toString())); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java index 66091da3ef..e2360aca2b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java @@ -19,7 +19,7 @@ package org.apache.cassandra.distributed.test; import java.util.concurrent.Callable; -import java.util.function.Supplier; +import java.util.concurrent.ExecutionException; import org.junit.Test; @@ -30,8 +30,9 @@ import net.bytebuddy.implementation.bind.annotation.SuperCall; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.streaming.StreamState; -import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.sequences.UnbootstrapStreams; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.apache.cassandra.db.SystemKeyspace.BootstrapState.COMPLETED; @@ -57,7 +58,7 @@ public class DecommissionTest extends TestBaseImpl .withInstanceInitializer(DecommissionTest.BB::install) .start())) { - IInvokableInstance instance = cluster.get(1); + IInvokableInstance instance = cluster.get(2); instance.runOnInstance(() -> { @@ -72,7 +73,7 @@ public class DecommissionTest extends TestBaseImpl } catch (Throwable t) { - assertEquals("simulated error in prepareUnbootstrapStreaming", t.getMessage()); + assertTrue(t.getMessage().contains("simulated error in prepareUnbootstrapStreaming")); } assertFalse(StorageService.instance.isDecommissioning()); @@ -101,23 +102,21 @@ public class DecommissionTest extends TestBaseImpl fail("the second decommission attempt should pass but it failed on: " + t.getMessage()); } - // check that decommissioning of already decommissioned node has no effect + assertEquals(DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); + assertFalse(StorageService.instance.isDecommissionFailed()); try { - assertEquals(DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); - assertFalse(StorageService.instance.isDecommissionFailed()); - StorageService.instance.decommission(true); - - assertEquals(DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); - assertFalse(StorageService.instance.isDecommissionFailed()); - assertFalse(StorageService.instance.isDecommissioning()); + fail("Should have failed since the node is in decomissioned state"); } - catch (Throwable t) + catch (UnsupportedOperationException e) { - fail("Decommissioning already decommissioned node should be no-op operation."); + // ignore } + assertEquals(DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); + assertFalse(StorageService.instance.isDecommissionFailed()); + assertFalse(StorageService.instance.isDecommissioning()); }); } } @@ -132,12 +131,12 @@ public class DecommissionTest extends TestBaseImpl // we do not want to install BB after restart of a node which // failed to decommission which is the second generation, here // as "1" as it is counted from 0. - if (num == 1 && generation != 1) + if (num == 2 && generation != 1) BB.install(classLoader, num); }) .start())) { - IInvokableInstance instance = cluster.get(1); + IInvokableInstance instance = cluster.get(2); instance.runOnInstance(() -> { assertEquals(COMPLETED.name(), StorageService.instance.getBootstrapState()); @@ -151,7 +150,7 @@ public class DecommissionTest extends TestBaseImpl } catch (Throwable t) { - assertEquals("simulated error in prepareUnbootstrapStreaming", t.getMessage()); + assertTrue(t.getMessage().contains("simulated error in prepareUnbootstrapStreaming")); } // node is in DECOMMISSION_FAILED mode @@ -169,15 +168,7 @@ public class DecommissionTest extends TestBaseImpl assertEquals(NORMAL.name(), oprationMode); instance.runOnInstance(() -> { - try - { - StorageService.instance.decommission(true); - } - catch (InterruptedException e) - { - fail("Should decommission the node"); - } - + StorageService.instance.decommission(true); assertEquals(DECOMMISSIONED.name(), StorageService.instance.getBootstrapState()); assertFalse(StorageService.instance.isDecommissionFailed()); assertFalse(StorageService.instance.isDecommissioning()); @@ -190,26 +181,26 @@ public class DecommissionTest extends TestBaseImpl { public static void install(ClassLoader classLoader, Integer num) { - new ByteBuddy().rebase(StorageService.class) - .method(named("prepareUnbootstrapStreaming")) - .intercept(MethodDelegation.to(DecommissionTest.BB.class)) - .make() - .load(classLoader, ClassLoadingStrategy.Default.INJECTION); + if (num == 2) + { + new ByteBuddy().rebase(UnbootstrapStreams.class) + .method(named("execute")) + .intercept(MethodDelegation.to(DecommissionTest.BB.class)) + .make() + .load(classLoader, ClassLoadingStrategy.Default.INJECTION); + } } - private static int invocations = 0; - @SuppressWarnings("unused") - public static Supplier> prepareUnbootstrapStreaming(@SuperCall Callable>> zuper) + public static void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave, + @SuperCall Callable zuper) throws ExecutionException, InterruptedException { - ++invocations; - - if (invocations == 1) - throw new RuntimeException("simulated error in prepareUnbootstrapStreaming"); + if (!StorageService.instance.isDecommissionFailed()) + throw new ExecutionException(new RuntimeException("simulated error in prepareUnbootstrapStreaming")); try { - return zuper.call(); + zuper.call(); } catch (Exception e) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java b/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java index ff8234d7d3..49646e0931 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FailureLoggingTest.java @@ -72,25 +72,6 @@ public class FailureLoggingTest extends TestBaseImpl } - @Test - public void testRequestBootstrapFail() throws Throwable - { - cluster.get(1).callOnInstance(() -> BBRequestFailures.bootstrapping = true); - long mark = cluster.get(1).logs().mark(); - - try - { - cluster.coordinator(1).execute("select * from " + KEYSPACE + ".tbl where id = 55", ConsistencyLevel.ALL); - fail("Query should fail"); - } - catch (RuntimeException e) - { - LogResult> result = cluster.get(1).logs().grep(mark, "while executing SELECT"); - assertEquals(1, result.getResult().size()); - assertTrue(result.getResult().get(0).contains("Cannot read from a bootstrapping node")); - } - } - @Test public void testRangeRequestFail() throws Throwable { diff --git a/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java index 3314c2a6ba..b6fc42c668 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FrozenUDTTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; @@ -131,8 +132,8 @@ public class FrozenUDTTest extends TestBaseImpl { cluster.schemaChange("create type " + KEYSPACE + ".a (foo text)"); cluster.schemaChange("create table " + KEYSPACE + ".x (id int, ck frozen, i int, primary key (id, ck))"); - - cluster.get(1).executeInternal("alter type " + KEYSPACE + ".a add bar text"); + cluster.filters().verbs(Verb.TCM_REPLICATION.id).drop().on(); + cluster.coordinator(1).execute("alter type " + KEYSPACE + ".a add bar text", ConsistencyLevel.QUORUM); cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 1) + ", ? )", ConsistencyLevel.ALL, 1, 1); cluster.coordinator(1).execute("insert into " + KEYSPACE + ".x (id, ck, i) VALUES (?, " + json(1, 2) + ", ? )", ConsistencyLevel.ALL, diff --git a/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java b/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java index 39054118aa..bdcf3fad5a 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/GossipTest.java @@ -18,203 +18,86 @@ package org.apache.cassandra.distributed.test; -import java.io.Closeable; +import java.io.IOException; import java.net.InetSocketAddress; import java.util.Collection; -import java.util.concurrent.*; -import java.util.concurrent.locks.LockSupport; -import java.util.stream.Collectors; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; -import com.google.common.collect.Iterables; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Assert; import org.junit.Test; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.implementation.MethodDelegation; -import org.apache.cassandra.dht.Token; +import com.google.monitoring.runtime.instrumentation.common.util.concurrent.Uninterruptibles; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.*; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.shared.ClusterUtils; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.PendingRangeCalculatorService; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.streaming.StreamPlan; -import org.apache.cassandra.streaming.StreamResultFuture; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.transformations.PrepareMove; import org.assertj.core.api.Assertions; -import static net.bytebuddy.matcher.ElementMatchers.named; -import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.api.TokenSupplier.evenlyDistributedTokens; import static org.apache.cassandra.distributed.impl.DistributedTestSnitch.toCassandraInetAddressAndPort; -import static org.apache.cassandra.distributed.shared.ClusterUtils.runAndWaitForLogs; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.replaceHostAndStart; +import static org.apache.cassandra.distributed.shared.ClusterUtils.stopUnchecked; import static org.apache.cassandra.distributed.shared.NetworkTopology.singleDcNetworkTopology; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class GossipTest extends TestBaseImpl { - @Test - public void nodeDownDuringMove() throws Throwable - { - int liveCount = 1; - try (Cluster cluster = Cluster.build(2 + liveCount) - .withConfig(config -> config.with(NETWORK).with(GOSSIP)) - .createWithoutStarting()) - { - int fail = liveCount + 1; - int late = fail + 1; - for (int i = 1 ; i <= liveCount ; ++i) - cluster.get(i).startup(); - cluster.get(fail).startup(); - Collection expectTokens = - cluster.get(fail) - .callsOnInstance(() -> StorageService.instance.getTokenMetadata() - .getTokens(FBUtilities.getBroadcastAddressAndPort()) - .stream() - .map(Object::toString) - .collect(Collectors.toList())) - .call(); - - InetSocketAddress failAddress = cluster.get(fail).broadcastAddress(); - // wait for NORMAL state - for (int i = 1 ; i <= liveCount ; ++i) - { - cluster.get(i).acceptsOnInstance((InetSocketAddress address) -> { - EndpointState ep; - InetAddressAndPort endpoint = toCassandraInetAddressAndPort(address); - while (null == (ep = Gossiper.instance.getEndpointStateForEndpoint(endpoint)) - || ep.getApplicationState(ApplicationState.STATUS_WITH_PORT) == null - || !ep.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.startsWith("NORMAL")) - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L)); - }).accept(failAddress); - } - - // set ourselves to MOVING, and wait for it to propagate - cluster.get(fail).runOnInstance(() -> { - Token token = Iterables.getFirst(StorageService.instance.getTokenMetadata().getTokens(FBUtilities.getBroadcastAddressAndPort()), null); - Gossiper.instance.addLocalApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.moving(token)); - }); - for (int i = 1 ; i <= liveCount ; ++i) - { - cluster.get(i).acceptsOnInstance((InetSocketAddress address) -> { - EndpointState ep; - InetAddressAndPort endpoint = toCassandraInetAddressAndPort(address); - while (null == (ep = Gossiper.instance.getEndpointStateForEndpoint(endpoint)) - || (ep.getApplicationState(ApplicationState.STATUS_WITH_PORT) == null - || !ep.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.startsWith("MOVING"))) - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100L)); - }).accept(failAddress); - } - - ClusterUtils.stopAbrupt(cluster, cluster.get(fail)); - cluster.get(late).startup(); - cluster.get(late).acceptsOnInstance((InetSocketAddress address) -> { - EndpointState ep; - InetAddressAndPort endpoint = toCassandraInetAddressAndPort(address); - while (null == (ep = Gossiper.instance.getEndpointStateForEndpoint(endpoint)) - || !ep.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.startsWith("MOVING")) - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100L)); - }).accept(failAddress); - - Collection tokens = - cluster.get(late) - .appliesOnInstance((InetSocketAddress address) -> - StorageService.instance.getTokenMetadata() - .getTokens(toCassandraInetAddressAndPort(address)) - .stream() - .map(Object::toString) - .collect(Collectors.toList())) - .apply(failAddress); - - Assert.assertEquals(expectTokens, tokens); - } - } - - public static class BBBootstrapInterceptor - { - final static CountDownLatch bootstrapReady = new CountDownLatch(1); - final static CountDownLatch bootstrapStart = new CountDownLatch(1); - static void install(ClassLoader cl, int nodeNumber) - { - if (nodeNumber != 2) - return; - new ByteBuddy().rebase(StorageService.class) - .method(named("bootstrap").and(takesArguments(2))) - .intercept(MethodDelegation.to(BBBootstrapInterceptor.class)) - .make() - .load(cl, ClassLoadingStrategy.Default.INJECTION); - } - - public static boolean bootstrap(Collection tokens, long bootstrapTimeoutMillis) - { - bootstrapStart.countDown(); - Uninterruptibles.awaitUninterruptibly(bootstrapReady); - return false; // bootstrap fails - } - } - @Test public void testPreventStoppingGossipDuringBootstrap() throws Exception { - ExecutorService es = Executors.newFixedThreadPool(1); try (Cluster cluster = builder().withNodes(2) .withConfig(config -> config.with(GOSSIP) .with(NETWORK) .set("auto_bootstrap", true)) - .withInstanceInitializer(BBBootstrapInterceptor::install) - .createWithoutStarting(); - Closeable ignored = es::shutdown) + .createWithoutStarting()) { - Runnable test = () -> - { + cluster.get(1).startup(); + Callable pending = ClusterUtils.pauseBeforeCommit(cluster.get(1), (e) -> e.kind() == Transformation.Kind.MID_JOIN); + Thread startup = new Thread(() -> cluster.get(2).startup()); + startup.start(); + pending.call(); + + cluster.get(2).runOnInstance(() -> { try { - cluster.get(2).runOnInstance(() -> { - Uninterruptibles.awaitUninterruptibly(BBBootstrapInterceptor.bootstrapStart); - BBBootstrapInterceptor.bootstrapReady.countDown(); - try - { - StorageService.instance.stopGossiping(); - - Assert.fail("stopGossiping did not fail!"); - } - catch (Exception ex) - { - Assert.assertSame(ex.getClass(), IllegalStateException.class); - Assert.assertEquals(ex.getMessage(), "Unable to stop gossip because the node is not in the normal state. Try to stop the node instead."); - } - }); + StorageService.instance.stopGossiping(); + fail("stopGossiping did not fail!"); } - finally + catch (Exception ex) { - // shut down the node2 to speed up cluster startup. - cluster.get(2).shutdown(); + Assert.assertSame(ex.getClass(), IllegalStateException.class); + Assert.assertEquals(ex.getMessage(), "Unable to stop gossip because the node is not in the normal state. Try to stop the node instead."); } - }; - Future testResult = es.submit(test); - try - { - cluster.startup(); - } - catch (Exception ex) { - // ignore exceptions from startup process. More interested in the test result. - } - testResult.get(); + }); + ClusterUtils.unpauseCommits(cluster.get(1)); + startup.join(); } - - es.awaitTermination(5, TimeUnit.SECONDS); } @Test @@ -227,12 +110,16 @@ public class GossipTest extends TestBaseImpl { cluster.get(1).runOnInstance(() -> { StorageService.instance.stopGossiping(); - StorageService.instance.setMovingModeUnsafe(); + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new PrepareMove(metadata.myNodeId(), + Collections.singleton(metadata.partitioner.getRandomToken()), + new UniformRangePlacement(), + false)); try { StorageService.instance.startGossiping(); - Assert.fail("startGossiping did not fail!"); + fail("startGossiping did not fail!"); } catch (Exception ex) { @@ -249,7 +136,6 @@ public class GossipTest extends TestBaseImpl // TODO: fails with vnode enabled try (Cluster cluster = Cluster.build(3) .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK)) - .withInstanceInitializer(FailureHelper::installMoveFailure) .withoutVNodes() .start()) { @@ -265,22 +151,24 @@ public class GossipTest extends TestBaseImpl long t2 = Long.parseLong(getLocalToken(node2)); long t3 = Long.parseLong(getLocalToken(node3)); long moveTo = t2 + ((t3 - t2)/2); - String logMsg = "Node " + node2.broadcastAddress() + " state moving, new token " + moveTo; - runAndWaitForLogs(() -> node2.nodetoolResult("move", "--", Long.toString(moveTo)).asserts().failure(), - logMsg, - cluster); + Callable pending = pauseBeforeCommit(node1, (e) -> e.kind() == Transformation.Kind.MID_MOVE); + new Thread(() -> node2.nodetoolResult("move", "--", Long.toString(moveTo)).asserts().failure()).start(); + pending.call(); InetSocketAddress movingAddress = node2.broadcastAddress(); + ClusterUtils.waitForCMSToQuiesce(cluster, node1); // node1 & node3 should now consider some ranges pending for node2 assertPendingRangesForPeer(true, movingAddress, cluster); - // A controlled shutdown causes peers to replace the MOVING status to be with SHUTDOWN, but prior to - // CASSANDRA-16796 this doesn't update TokenMetadata, so they maintain pending ranges for the down node - // indefinitely, even after it has been removed from the ring. - logMsg = "Marked " + node2.broadcastAddress() + " as shutdown"; - runAndWaitForLogs(() -> Futures.getUnchecked(node2.shutdown()), - logMsg, - node1, node3); + ClusterUtils.unpauseCommits(node1); + node2.shutdown(); + + node1.acceptsOnInstance((InetSocketAddress addr) -> { + ClusterMetadata metadata = ClusterMetadata.current(); + StorageService.instance.cancelInProgressSequences(metadata.directory.peerId(InetAddressAndPort.getByAddress(addr))); + }).accept(node2.broadcastAddress()); + + ClusterUtils.waitForCMSToQuiesce(cluster, node1); // node1 & node3 should not consider any ranges as still pending for node2 assertPendingRangesForPeer(false, movingAddress, cluster); } @@ -331,17 +219,11 @@ public class GossipTest extends TestBaseImpl boolean hasPending = inst.appliesOnInstance((InetSocketAddress address) -> { InetAddressAndPort peer = toCassandraInetAddressAndPort(address); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - boolean isMoving = StorageService.instance.getTokenMetadata() - .getMovingEndpoints() + boolean isMoving = StorageService.instance.endpointsWithState(NodeState.MOVING) .stream() - .map(pair -> pair.right) .anyMatch(peer::equals); - return isMoving && !StorageService.instance.getTokenMetadata() - .getPendingRanges(KEYSPACE, peer) - .isEmpty(); + return isMoving && ClusterMetadata.current().hasPendingRangesFor(Keyspace.open(KEYSPACE).getMetadata(), peer); }).apply(movingAddress); assertEquals(String.format("%s should %shave PENDING RANGES for %s", inst.broadcastAddress().getHostString(), @@ -362,23 +244,44 @@ public class GossipTest extends TestBaseImpl } } - public static class FailureHelper + @Test + public void testQuarantine() throws IOException { - static void installMoveFailure(ClassLoader cl, int nodeNumber) + TokenSupplier even = TokenSupplier.evenlyDistributedTokens(4, 1); + try (Cluster cluster = Cluster.build(4) + .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK) + .set("progress_barrier_default_consistency_level", "ONE") + .set("progress_barrier_min_consistency_level", "ONE")) + .withTokenSupplier(node -> even.token(node == 5 ? 4 : node)) + .start()) { - if (nodeNumber == 2) + // 4 nodes, stop node3 from catching up + cluster.filters().verbs(Verb.TCM_REPLICATION.id).to(3).drop(); + IInvokableInstance toRemove = cluster.get(4); + String node4 = toRemove.config().broadcastAddress().getAddress().getHostAddress(); + stopUnchecked(toRemove); + replaceHostAndStart(cluster, toRemove); + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); // wait a few gossip rounds + cluster.get(2).runOnInstance(() -> assertFalse(Gossiper.instance.endpointStateMap.containsKey(InetAddressAndPort.getByNameUnchecked(node4)))); + cluster.get(3).runOnInstance(() -> assertFalse(Gossiper.instance.endpointStateMap.containsKey(InetAddressAndPort.getByNameUnchecked(node4)))); + cluster.get(3).nodetoolResult("disablegossip").asserts().success(); + cluster.get(3).nodetoolResult("enablegossip").asserts().success(); + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); + cluster.get(2).runOnInstance(() -> assertFalse(Gossiper.instance.endpointStateMap.containsKey(InetAddressAndPort.getByNameUnchecked(node4)))); + cluster.filters().reset(); + long epoch = cluster.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + cluster.get(3).runOnInstance(() -> ClusterMetadataService.instance().fetchLogFromCMS(Epoch.create(epoch))); + boolean removed = false; + for (int i = 0; i < 20; i++) { - new ByteBuddy().redefine(StreamPlan.class) - .method(named("execute")) - .intercept(MethodDelegation.to(FailureHelper.class)) - .make() - .load(cl, ClassLoadingStrategy.Default.INJECTION); + if (!cluster.get(3).callOnInstance(() -> Gossiper.instance.endpointStateMap.containsKey(InetAddressAndPort.getByNameUnchecked(node4)))) + { + removed = true; + break; + } + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); } - } - - public static StreamResultFuture execute() - { - throw new RuntimeException("failing to execute move"); + assertTrue(removed); } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/HintedHandoffAddRemoveNodesTest.java b/test/distributed/org/apache/cassandra/distributed/test/HintedHandoffAddRemoveNodesTest.java index add6fdf500..3ebed55ef4 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/HintedHandoffAddRemoveNodesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/HintedHandoffAddRemoveNodesTest.java @@ -18,25 +18,24 @@ package org.apache.cassandra.distributed.test; -import org.awaitility.Awaitility; import org.junit.Test; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.action.GossipHelper; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.shared.WithProperties; import org.apache.cassandra.metrics.HintsServiceMetrics; import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.service.StorageService; +import org.awaitility.Awaitility; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.awaitility.Awaitility.await; -import static org.junit.Assert.assertEquals; - -import static org.apache.cassandra.distributed.action.GossipHelper.decommission; +import static org.apache.cassandra.config.DatabaseDescriptor.setProgressBarrierMinConsistencyLevel; +import static org.apache.cassandra.db.ConsistencyLevel.NODE_LOCAL; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; import static org.apache.cassandra.distributed.api.ConsistencyLevel.TWO; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; @@ -44,6 +43,9 @@ import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.AssertUtils.row; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; /** * Tests around removing and adding nodes from and to a cluster while hints are still outstanding. @@ -55,8 +57,13 @@ public class HintedHandoffAddRemoveNodesTest extends TestBaseImpl public void shouldAvoidHintTransferOnDecommission() throws Exception { try (Cluster cluster = init(builder().withNodes(3) - .withConfig(config -> config.set("transfer_hints_on_decommission", false).with(GOSSIP)) - .withoutVNodes() + .withConfig(config -> config.set("transfer_hints_on_decommission", false) + .set("progress_barrier_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms") + // Just to make test pass faster + .set("progress_barrier_min_consistency_level", NODE_LOCAL) + .set("progress_barrier_default_consistency_level", NODE_LOCAL) + .with(GOSSIP)) .start())) { cluster.schemaChange(withKeyspace("CREATE TABLE %s.decom_no_hints_test (key int PRIMARY KEY, value int)")); @@ -75,9 +82,10 @@ public class HintedHandoffAddRemoveNodesTest extends TestBaseImpl long hintsAfterShutdown = countTotalHints(cluster.get(1)); assertThat(hintsAfterShutdown).isEqualTo(1); - cluster.get(1).nodetoolResult("decommission", "--force").asserts().success(); - long hintsDeliveredByDecom = countHintsDelivered(cluster.get(1)); - String mode = cluster.get(1).callOnInstance(() -> StorageService.instance.getOperationMode()); + cluster.get(2).runOnInstance(() -> setProgressBarrierMinConsistencyLevel(org.apache.cassandra.db.ConsistencyLevel.ONE)); + cluster.get(2).nodetoolResult("decommission", "--force").asserts().success(); + long hintsDeliveredByDecom = countHintsDelivered(cluster.get(2)); + String mode = cluster.get(2).callOnInstance(() -> StorageService.instance.getOperationMode()); assertEquals(StorageService.Mode.DECOMMISSIONED.toString(), mode); assertThat(hintsDeliveredByDecom).isEqualTo(0); } @@ -89,15 +97,18 @@ public class HintedHandoffAddRemoveNodesTest extends TestBaseImpl @Test public void shouldStreamHintsDuringDecommission() throws Exception { - try (Cluster cluster = builder().withNodes(4) - .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)) + try (Cluster cluster = builder().withNodes(5) + .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL) + // Just to make test pass faster + .set("progress_barrier_min_consistency_level", NODE_LOCAL) + .set("progress_barrier_default_consistency_level", NODE_LOCAL)) .withoutVNodes() .start()) { cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2}")); cluster.schemaChange(withKeyspace("CREATE TABLE %s.decom_hint_test (key int PRIMARY KEY, value int)")); - cluster.get(4).shutdown().get(); + cluster.get(5).shutdown().get(); // Write data using the second node as the coordinator... populate(cluster, "decom_hint_test", 2, 0, 128, ConsistencyLevel.ONE); @@ -106,33 +117,39 @@ public class HintedHandoffAddRemoveNodesTest extends TestBaseImpl Awaitility.await().until(() -> countTotalHints(cluster.get(2)) > 0); long totalHints = countTotalHints(cluster.get(2)); - // Decomision node 1... - assertEquals(4, endpointsKnownTo(cluster, 2)); - cluster.run(decommission(), 1); - await().pollDelay(1, SECONDS).until(() -> endpointsKnownTo(cluster, 2) == 3); + // Decomision node 2... + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + cluster.get(2).nodetoolResult("decommission", "--force").asserts().success(); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); // ...and verify that all data still exists on either node 2 or 3. - verify(cluster, "decom_hint_test", 2, 0, 128, ConsistencyLevel.ONE); + verify(cluster, "decom_hint_test", 1, 0, 128, ConsistencyLevel.ONE); - // Start node 4 back up and verify that all hints were delivered. - cluster.get(4).startup(); + // Start node 5 back up and verify that all hints were delivered. + cluster.get(5).startup(); await().atMost(30, SECONDS).pollDelay(3, SECONDS).until(() -> count(cluster, "decom_hint_test", 4) >= totalHints); - // Now decommission both nodes 2 and 3... - cluster.run(GossipHelper.decommission(true), 2); - cluster.run(GossipHelper.decommission(true), 3); - await().pollDelay(1, SECONDS).until(() -> endpointsKnownTo(cluster, 4) == 1); + // Now decommission both node 4 + cluster.get(3).nodetoolResult("decommission", "--force").asserts().success(); + cluster.get(4).nodetoolResult("decommission", "--force").asserts().success(); + cluster.get(5).nodetoolResult("decommission", "--force").asserts().success(); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1), 2, 3, 4, 5); // ...and verify that even if we drop below the replication factor of 2, all data has been preserved. - verify(cluster, "decom_hint_test", 4, 0, 128, ConsistencyLevel.ONE); + verify(cluster, "decom_hint_test", 1, 0, 128, ConsistencyLevel.ONE); } } @Test public void shouldBootstrapWithHintsOutstanding() throws Exception { - try (Cluster cluster = builder().withNodes(3) + try (WithProperties properties = new WithProperties().set(CassandraRelevantProperties.CONSISTENT_RANGE_MOVEMENT, "false"); + Cluster cluster = builder().withNodes(3) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4, 1)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)) + .withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL) + // Just to make test pass faster + .set("progress_barrier_min_consistency_level", NODE_LOCAL) + .set("progress_barrier_default_consistency_level", NODE_LOCAL)) .start()) { cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2}")); @@ -196,9 +213,4 @@ public class HintedHandoffAddRemoveNodesTest extends TestBaseImpl { return (Long) cluster.get(node).executeInternal("SELECT COUNT(*) FROM " + KEYSPACE + '.' + table)[0][0]; } - - private int endpointsKnownTo(Cluster cluster, int node) - { - return cluster.get(node).callOnInstance(() -> StorageService.instance.getTokenMetadata().getAllEndpoints().size()); - } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/IPMembershipTest.java b/test/distributed/org/apache/cassandra/distributed/test/IPMembershipTest.java index 16a97b005b..d72180f902 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/IPMembershipTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/IPMembershipTest.java @@ -26,6 +26,7 @@ import java.util.UUID; import com.google.common.collect.ImmutableSet; import org.junit.Test; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.api.Feature; @@ -36,7 +37,6 @@ import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.tools.ToolRunner; import org.assertj.core.api.Assertions; -import static org.apache.cassandra.config.CassandraRelevantProperties.REPLACE_ADDRESS; import static org.apache.cassandra.distributed.shared.ClusterUtils.assertRingIs; import static org.apache.cassandra.distributed.shared.ClusterUtils.getDirectories; import static org.apache.cassandra.distributed.shared.ClusterUtils.stopUnchecked; @@ -58,7 +58,7 @@ public class IPMembershipTest extends TestBaseImpl IInvokableInstance nodeToReplace = cluster.get(3); ToolRunner.invokeCassandraStress("write", "n=10000", "-schema", "replication(factor=3)", "-port", "native=9042").assertOnExitCode(); - + DatabaseDescriptor.toolInitialization(); // needed for deleteRecursive below for (boolean auto_bootstrap : Arrays.asList(true, false)) { stopUnchecked(nodeToReplace); @@ -69,9 +69,8 @@ public class IPMembershipTest extends TestBaseImpl // we need to override the host id because otherwise the node will not be considered as a new node ((InstanceConfig) nodeToReplace.config()).setHostId(UUID.randomUUID()); - Assertions.assertThatThrownBy(() -> nodeToReplace.startup()) - .hasMessage("A node with address /127.0.0.3:7012 already exists, cancelling join. Use " + - REPLACE_ADDRESS.getKey() + " if you want to replace this node."); + Assertions.assertThatThrownBy(nodeToReplace::startup) + .hasMessage("A node with address /127.0.0.3:7012 already exists, cancelling join. Use cassandra.replace_address if you want to replace this node."); } } } @@ -80,7 +79,7 @@ public class IPMembershipTest extends TestBaseImpl * Tests the behavior if a node restarts with a different IP. */ @Test - public void startupNewIP() throws IOException, InterruptedException + public void startupNewIP() throws IOException { try (Cluster cluster = Cluster.build(3) .withConfig(c -> c.with(Feature.GOSSIP, Feature.NATIVE_PROTOCOL) diff --git a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java index 27c26fb2c6..a37f092a74 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionEnforcementTest.java @@ -24,15 +24,21 @@ import java.io.InputStream; import java.net.InetAddress; import java.security.KeyStore; import java.security.cert.Certificate; +import java.util.Collections; import java.util.HashMap; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.collect.ImmutableMap; import org.junit.Test; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; import org.apache.cassandra.auth.AllowAllInternodeAuthenticator; import org.apache.cassandra.auth.IInternodeAuthenticator; import org.apache.cassandra.config.DatabaseDescriptor; @@ -41,22 +47,29 @@ import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableRunnable; import org.apache.cassandra.distributed.shared.NetworkTopology; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.ConnectionType; import org.apache.cassandra.net.InboundMessageHandlers; +import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.OutboundConnections; +import org.apache.cassandra.net.PingRequest; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; import org.awaitility.Awaitility; import static com.google.common.collect.Iterables.getOnlyElement; -import static org.hamcrest.Matchers.containsString; +import static net.bytebuddy.matcher.ElementMatchers.named; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +// Nodes start communicating rather early with TCM, which means that we will encouter the expected exceptions already on startup. This test should simply be rewritten to check +// communication between non-CMS nodes. public final class InternodeEncryptionEnforcementTest extends TestBaseImpl { - @Test public void testInboundConnectionsAreRejectedWhenAuthFails() throws IOException, TimeoutException { @@ -64,9 +77,10 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl Cluster.Builder builder = createCluster(RejectInboundConnections.class); final ExecutorService executorService = Executors.newSingleThreadExecutor(); - try (Cluster cluster = builder.start(); Closeable es = executorService::shutdown) + try (Cluster cluster = builder.withInstanceInitializer(BB::install).start(); + Closeable es = executorService::shutdown) { - executorService.submit(() -> openConnections(cluster)); + openConnections(cluster, true); /* * Instance (1) should be able to make outbound connections to instance (2) but Instance (1) should not be @@ -74,8 +88,8 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl */ SerializableRunnable runnable = () -> { - // There should be no inbound handlers as authentication fails & we remove handlers. - assertEquals(0, MessagingService.instance().messageHandlers.values().size()); + // There should be no inbound handlers as authentication fails and we remove handlers. + assertTrue(MessagingService.instance().messageHandlers.isEmpty()); // Verify that the failure is due to authentication failure final RejectInboundConnections authenticator = (RejectInboundConnections) DatabaseDescriptor.getInternodeAuthenticator(); @@ -95,9 +109,10 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl Cluster.Builder builder = createCluster(RejectOutboundAuthenticator.class); final ExecutorService executorService = Executors.newSingleThreadExecutor(); - try (Cluster cluster = builder.start(); Closeable es = executorService::shutdown) + try (Cluster cluster = builder.withInstanceInitializer(BB::install).start(); + Closeable es = executorService::shutdown) { - executorService.submit(() -> openConnections(cluster)); + openConnections(cluster, true); /* * Instance (1) should not be able to make outbound connections to instance (2) but Instance (2) should be @@ -125,14 +140,19 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl { Cluster.Builder builder = createCluster(AllowFirstAndRejectOtherOutboundAuthenticator.class); final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - try (Cluster cluster = builder.start(); Closeable es = executorService::shutdown) + try (Cluster cluster = builder.withInstanceInitializer(BB::install).start(); + Closeable es = executorService::shutdown) { - executorService.submit(() -> openConnections(cluster)); + long mark = cluster.get(1).logs().mark(); + executorService.submit(() -> { + openConnections(cluster, 2, 1, Verb.PING_REQ, ConnectionType.LARGE_MESSAGES, false); + openConnections(cluster, 1, 2, Verb.PING_REQ, ConnectionType.SMALL_MESSAGES, true); + + }); // Verify that authentication is failed and Interrupt is called on outbound connections. - cluster.get(1).logs().watchFor("Authentication failed to"); - cluster.get(1).logs().watchFor("Interrupted outbound connections to"); + cluster.get(1).logs().watchFor(mark, "Authentication failed to"); + cluster.get(1).logs().watchFor(mark, "Interrupted outbound connections to"); /* * Check if outbound connections are zero @@ -190,17 +210,9 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl .withNodeIdTopology(ImmutableMap.of(1, NetworkTopology.dcAndRack("dc1", "r1a"), 2, NetworkTopology.dcAndRack("dc2", "r2a"))); - try (Cluster cluster = builder.start()) + try (Cluster cluster = builder.withInstanceInitializer(BB::install).start()) { - try - { - openConnections(cluster); - fail("Instances should not be able to connect, much less complete a schema change."); - } - catch (RuntimeException ise) - { - assertThat(ise.getMessage(), containsString("agreement not reached")); - } + openConnections(cluster, true); /* * instance (1) won't connect to (2), since (2) won't have a TLS listener; @@ -229,6 +241,31 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl } } + /** + * Some tests require nodes to be fully started up before they open connections to other nodes (i.e. cluster has to be + * fully started). This allows each node to fully start up thinking it is the only seed in the cluster. Since tests + * do not expect nodes to successfully commit, this is the simplest way to test such things. + */ + public static class BB + { + public static void install(ClassLoader classLoader, Integer num) + { + new ByteBuddy().rebase(DatabaseDescriptor.class) + .method(named("getSeeds")) + .intercept(MethodDelegation.to(BB.class)) + .make() + .load(classLoader, ClassLoadingStrategy.Default.INJECTION); + + } + + @SuppressWarnings("unused") + public static Set getSeeds() + { + return Collections.singleton(FBUtilities.getBroadcastAddressAndPort()); + } + } + + @Test public void testConnectionsAreAcceptedWithValidConfig() throws Throwable { @@ -272,13 +309,52 @@ public final class InternodeEncryptionEnforcementTest extends TestBaseImpl } } + private void openConnections(Cluster cluster, boolean expectFail) + { + openConnections(cluster, Verb.PING_REQ, ConnectionType.SMALL_MESSAGES, expectFail); + } + + private void openConnections(Cluster cluster, Verb verb, ConnectionType connectionType, boolean expectFail) + { + Pair[] connections = new Pair[] { Pair.create(1, 2), Pair.create(2, 1) }; + for (Pair connection : connections) + openConnections(cluster, connection.left, connection.right, verb, connectionType, expectFail); + } + + private void openConnections(Cluster cluster, int from, int to, Verb verb, ConnectionType connectionType, boolean expectFail) + { + boolean failed = false; + try + { + cluster.get(from).acceptsOnInstance((Integer p, Integer ct) -> { + try + { + MessagingService.instance().sendWithResponse(InetAddressAndPort.getByName("127.0.0." + p), + Message.out(verb, PingRequest.get(ConnectionType.fromId(ct)))) + .get(5, TimeUnit.SECONDS); + } + catch (Throwable e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + }).accept(to, connectionType.id); + } + catch (Throwable t) + { + failed = true; + } + if (expectFail != failed) + fail(String.format("Should %shave failed", expectFail ? "" : "not ")); + } + private void openConnections(Cluster cluster) { cluster.schemaChange("CREATE KEYSPACE test_connections_from_1 " + - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};", false, cluster.get(1)); + "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};", false, cluster.get(1), 5, TimeUnit.SECONDS); cluster.schemaChange("CREATE KEYSPACE test_connections_from_2 " + - "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};", false, cluster.get(2)); + "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};", false, cluster.get(2), 5, TimeUnit.SECONDS); } private void verifyAuthenticationSucceeds(final Class authenticatorClass) throws IOException diff --git a/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java b/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java index f11ad16bb7..c764df16bf 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/JVMDTestTest.java @@ -173,9 +173,13 @@ public class JVMDTestTest extends TestBaseImpl assertRows(cluster.get(2).executeInternal("SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?", KEYSPACE), row("tbl1"), row("tbl2"), row("tbl3")); - // Finally test schema can be changed with the first node down - cluster.get(1).shutdown(true).get(1, TimeUnit.MINUTES); + // Finally test schema can be changed with the non-CMS node down + cluster.get(2).shutdown(true).get(1, TimeUnit.MINUTES); cluster.schemaChangeIgnoringStoppedInstances("CREATE TABLE "+KEYSPACE+".tbl4 (id int primary key, i int)"); + assertRows(cluster.get(1).executeInternal("SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?", KEYSPACE), + row("tbl1"), row("tbl2"), row("tbl3"), row("tbl4")); + // Restart the down node and check it catches up with the new schema + cluster.get(2).startup(); assertRows(cluster.get(2).executeInternal("SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?", KEYSPACE), row("tbl1"), row("tbl2"), row("tbl3"), row("tbl4")); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/MigrationCoordinatorTest.java b/test/distributed/org/apache/cassandra/distributed/test/MigrationCoordinatorTest.java deleted file mode 100644 index e75783d497..0000000000 --- a/test/distributed/org/apache/cassandra/distributed/test/MigrationCoordinatorTest.java +++ /dev/null @@ -1,123 +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.distributed.test; - -import java.net.InetAddress; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.shared.NetworkTopology; -import org.apache.cassandra.schema.Schema; - -import static org.apache.cassandra.config.CassandraRelevantProperties.CONSISTENT_RANGE_MOVEMENT; -import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_ENDPOINTS; -import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_VERSIONS; -import static org.apache.cassandra.config.CassandraRelevantProperties.REPLACE_ADDRESS; -import static org.apache.cassandra.distributed.api.Feature.GOSSIP; -import static org.apache.cassandra.distributed.api.Feature.NETWORK; - -public class MigrationCoordinatorTest extends TestBaseImpl -{ - - @Before - public void setUp() - { - REPLACE_ADDRESS.clearValue(); // checkstyle: suppress nearby 'clearValueSystemPropertyUsage' - CONSISTENT_RANGE_MOVEMENT.clearValue(); // checkstyle: suppress nearby 'clearValueSystemPropertyUsage' - IGNORED_SCHEMA_CHECK_VERSIONS.clearValue(); // checkstyle: suppress nearby 'clearValueSystemPropertyUsage' - } - /** - * We shouldn't wait on versions only available from a node being replaced - * see CASSANDRA- - */ - @Test - public void replaceNode() throws Throwable - { - try (Cluster cluster = Cluster.build(2) - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP)) - .start()) - { - cluster.schemaChange("CREATE KEYSPACE ks with replication={'class':'SimpleStrategy', 'replication_factor':2}"); - InetAddress replacementAddress = cluster.get(2).broadcastAddress().getAddress(); - cluster.get(2).shutdown(false); - cluster.schemaChangeIgnoringStoppedInstances("CREATE TABLE ks.tbl (k int primary key, v int)"); - - IInstanceConfig config = cluster.newInstanceConfig(); - config.set("auto_bootstrap", true); - REPLACE_ADDRESS.setString(replacementAddress.getHostAddress()); - cluster.bootstrap(config).startup(); - } - } - - @Test - public void explicitEndpointIgnore() throws Throwable - { - try (Cluster cluster = Cluster.build(2) - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP)) - .start()) - { - cluster.schemaChange("CREATE KEYSPACE ks with replication={'class':'SimpleStrategy', 'replication_factor':2}"); - InetAddress ignoredEndpoint = cluster.get(2).broadcastAddress().getAddress(); - cluster.get(2).shutdown(false); - cluster.schemaChangeIgnoringStoppedInstances("CREATE TABLE ks.tbl (k int primary key, v int)"); - - IInstanceConfig config = cluster.newInstanceConfig(); - config.set("auto_bootstrap", true); - IGNORED_SCHEMA_CHECK_ENDPOINTS.setString(ignoredEndpoint.getHostAddress()); - CONSISTENT_RANGE_MOVEMENT.setBoolean(false); - cluster.bootstrap(config).startup(); - } - } - - @Test - public void explicitVersionIgnore() throws Throwable - { - try (Cluster cluster = Cluster.build(2) - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP)) - .start()) - { - UUID initialVersion = cluster.get(2).callsOnInstance(() -> Schema.instance.getVersion()).call(); - cluster.schemaChange("CREATE KEYSPACE ks with replication={'class':'SimpleStrategy', 'replication_factor':2}"); - UUID oldVersion; - do - { - oldVersion = cluster.get(2).callsOnInstance(() -> Schema.instance.getVersion()).call(); - } while (oldVersion.equals(initialVersion)); - cluster.get(2).shutdown(false); - cluster.schemaChangeIgnoringStoppedInstances("CREATE TABLE ks.tbl (k int primary key, v int)"); - - IInstanceConfig config = cluster.newInstanceConfig(); - config.set("auto_bootstrap", true); - IGNORED_SCHEMA_CHECK_VERSIONS.setString(initialVersion.toString() + ',' + oldVersion); - CONSISTENT_RANGE_MOVEMENT.setBoolean(false); - cluster.bootstrap(config).startup(); - } - } -} diff --git a/test/distributed/org/apache/cassandra/distributed/test/MixedModeFuzzTest.java b/test/distributed/org/apache/cassandra/distributed/test/MixedModeFuzzTest.java index 954280f0b8..1f7f388d34 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/MixedModeFuzzTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/MixedModeFuzzTest.java @@ -57,6 +57,8 @@ import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.impl.RowUtil; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.CassandraVersion; @@ -70,7 +72,7 @@ import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; import static org.apache.cassandra.distributed.api.Feature.NETWORK; -public class MixedModeFuzzTest extends TestBaseImpl +public class MixedModeFuzzTest extends FuzzTestBase { private static final Logger logger = LoggerFactory.getLogger(ReprepareFuzzTest.class); @@ -96,6 +98,7 @@ public class MixedModeFuzzTest extends TestBaseImpl { c.schemaChange(withKeyspace("CREATE KEYSPACE ks" + i + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); c.schemaChange(withKeyspace("CREATE TABLE ks" + i + ".tbl (pk int, ck int, PRIMARY KEY (pk, ck));")); + ClusterUtils.waitForCMSToQuiesce(c, c.get(1)); for (int j = 0; j < i; j++) c.coordinator(1).execute("INSERT INTO ks" + i + ".tbl (pk, ck) VALUES (?, ?)", ConsistencyLevel.ALL, 1, j); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/MoveTest.java b/test/distributed/org/apache/cassandra/distributed/test/MoveTest.java index cdbe1cd59b..0ce1b8be3b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/MoveTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/MoveTest.java @@ -48,7 +48,6 @@ public class MoveTest extends TestBaseImpl private void move(boolean forwards) throws Throwable { - // TODO: fails with vnode enabled try (Cluster cluster = Cluster.build(4) .withConfig(config -> config.set("paxos_variant", "v2_without_linearizable_reads").with(NETWORK).with(GOSSIP)) .withoutVNodes() @@ -63,9 +62,9 @@ public class MoveTest extends TestBaseImpl } List initialTokens = new ArrayList<>(); - for (int i=0; i Iterables.getOnlyElement(StorageService.instance.getLocalTokens()).toString()).call(); + String token = cluster.get(i).callsOnInstance(() -> Iterables.getOnlyElement(StorageService.instance.getLocalTokens()).toString()).call(); initialTokens.add(token); } Assert.assertEquals(Lists.newArrayList("-4611686018427387905", diff --git a/test/distributed/org/apache/cassandra/distributed/test/PartitionDenylistTest.java b/test/distributed/org/apache/cassandra/distributed/test/PartitionDenylistTest.java index d8e63784ed..4108ece08f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PartitionDenylistTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PartitionDenylistTest.java @@ -26,7 +26,6 @@ import java.util.concurrent.TimeoutException; import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Assert; import org.junit.Test; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java index a150a33a95..ed066b2c3b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java @@ -90,6 +90,7 @@ import org.apache.cassandra.service.paxos.uncommitted.PaxosUncommittedTracker.Up import org.apache.cassandra.streaming.PreviewKind; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.CloseableIterator; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; @@ -100,8 +101,6 @@ import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; import static org.apache.cassandra.service.paxos.Ballot.Flag.GLOBAL; import static org.apache.cassandra.service.paxos.BallotGenerator.Global.staleBallot; -import org.apache.cassandra.utils.CloseableIterator; - // quick workaround for metaspace ooms, will properly reuse clusters later public class PaxosRepair2Test extends TestBaseImpl { @@ -112,6 +111,7 @@ public class PaxosRepair2Test extends TestBaseImpl static { CassandraRelevantProperties.PAXOS_USE_SELF_EXECUTION.setBoolean(false); + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); DatabaseDescriptor.daemonInitialization(); } @@ -197,6 +197,7 @@ public class PaxosRepair2Test extends TestBaseImpl repair(cluster, KEYSPACE, TABLE); // stop and start node 2 to test loading paxos repair history from disk + cluster.get(2).flush(SYSTEM_KEYSPACE_NAME); cluster.get(2).shutdown().get(); cluster.get(2).startup(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepairTest.java index dd1aa07e20..6ec42d40b5 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepairTest.java @@ -51,16 +51,13 @@ import org.apache.cassandra.db.rows.*; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IMessageFilters; import org.apache.cassandra.gms.FailureDetector; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.schema.Schema; @@ -72,6 +69,11 @@ import org.apache.cassandra.service.paxos.*; import org.apache.cassandra.service.paxos.cleanup.PaxosCleanup; import org.apache.cassandra.service.paxos.uncommitted.PaxosRows; import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; import org.apache.cassandra.utils.*; import static java.util.concurrent.TimeUnit.SECONDS; @@ -97,6 +99,8 @@ public class PaxosRepairTest extends TestBaseImpl static { CassandraRelevantProperties.PAXOS_USE_SELF_EXECUTION.setBoolean(false); + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + CassandraRelevantProperties.TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES.setBoolean(true); // for paxosRepairVersionGate DatabaseDescriptor.daemonInitialization(); } @@ -117,7 +121,7 @@ public class PaxosRepairTest extends TestBaseImpl Set allEndpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.broadcastAddress())).collect(Collectors.toSet()); cluster.stream().forEach(instance -> { instance.runOnInstance(() -> { - ImmutableSet endpoints = Gossiper.instance.getEndpoints(); + ImmutableSet endpoints = ImmutableSet.copyOf(ClusterMetadata.current().directory.allJoinedEndpoints()); Assert.assertEquals(allEndpoints, endpoints); for (InetAddressAndPort endpoint : endpoints) Assert.assertTrue(FailureDetector.instance.isAlive(endpoint)); @@ -501,21 +505,22 @@ public class PaxosRepairTest extends TestBaseImpl } } - private static void setVersion(IInvokableInstance instance, InetSocketAddress peer, String version) + private static void setVersion(ICluster cluster, InetSocketAddress peer, String version) { - instance.runOnInstance(() -> { - Gossiper.runInGossipStageBlocking(() -> { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(InetAddressAndPort.getByAddress(peer.getAddress())); - VersionedValue value = version != null ? StorageService.instance.valueFactory.rack(version) : null; - epState.addApplicationState(ApplicationState.RELEASE_VERSION, value); - }); - }); + cluster.get(1).acceptsOnInstance((InetSocketAddress addr) -> { + ClusterMetadata cm = ClusterMetadata.current(); + Directory directory = cm.directory.withNodeVersion(cm.directory.peerId(InetAddressAndPort.getByAddress(addr)), + new NodeVersion(new CassandraVersion(version), NodeVersion.CURRENT_METADATA_VERSION)); + + ClusterMetadata nextMetadata = cm.transformer().with(directory).build().metadata; + ClusterMetadataService.instance().commit(new ForceSnapshot(nextMetadata)); + }).accept(cluster.get(2).broadcastAddress()); } private static void assertRepairFailsWithVersion(Cluster cluster, String version) { - for (int i = 1 ; i <= cluster.size() ; ++i) - setVersion(cluster.get(i), cluster.get(2).broadcastAddress(), version); + setVersion(cluster, cluster.get(2).broadcastAddress(), version); + try { repair(cluster, KEYSPACE, TABLE); @@ -529,8 +534,7 @@ public class PaxosRepairTest extends TestBaseImpl private static void assertRepairSucceedsWithVersion(Cluster cluster, String version) { - for (int i = 1 ; i <= cluster.size() ; ++i) - setVersion(cluster.get(i), cluster.get(2).broadcastAddress(), version); + setVersion(cluster, cluster.get(2).broadcastAddress(), version); repair(cluster, KEYSPACE, TABLE); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/PaxosUncommittedIndexTest.java b/test/distributed/org/apache/cassandra/distributed/test/PaxosUncommittedIndexTest.java index 0ef52e7d3d..4f2ef48126 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PaxosUncommittedIndexTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PaxosUncommittedIndexTest.java @@ -51,4 +51,4 @@ public class PaxosUncommittedIndexTest extends TestBaseImpl } } } -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java index 83c2806079..7d459d511d 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java @@ -18,22 +18,23 @@ package org.apache.cassandra.distributed.test; -import java.net.InetSocketAddress; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.ExecutionException; -import org.apache.cassandra.utils.concurrent.Condition; +import com.google.common.util.concurrent.FutureCallback; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Mutation; @@ -48,13 +49,13 @@ import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.shared.NetworkTopology; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; -import org.apache.cassandra.service.PendingRangeCalculatorService; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.repair.BlockingReadRepair; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; +import org.apache.cassandra.utils.concurrent.Condition; +import org.apache.cassandra.utils.concurrent.Future; +import org.checkerframework.checker.nullness.qual.Nullable; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -177,9 +178,10 @@ public class ReadRepairTest extends TestBaseImpl } } - @Test + @Test @Ignore public void movingTokenReadRepairTest() throws Throwable { + // TODO: rewrite using FuzzTestBase to control progress through decommission // TODO: fails with vnode enabled try (Cluster cluster = init(Cluster.build(4).withoutVNodes().start(), 3)) { @@ -200,11 +202,11 @@ public class ReadRepairTest extends TestBaseImpl // write only to #4 cluster.get(4).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, 1, 1)", i); // mark #2 as leaving in #4 - cluster.get(4).acceptsOnInstance((InetSocketAddress endpoint) -> { - StorageService.instance.getTokenMetadata().addLeavingEndpoint(InetAddressAndPort.getByAddressOverrideDefaults(endpoint.getAddress(), endpoint.getPort())); - PendingRangeCalculatorService.instance.update(); - PendingRangeCalculatorService.instance.blockUntilFinished(); - }).accept(cluster.get(2).broadcastAddress()); +// cluster.get(4).acceptsOnInstance((InetSocketAddress endpoint) -> { +//// StorageService.instance.getTokenMetadata().addLeavingEndpoint(InetAddressAndPort.getByAddressOverrideDefaults(endpoint.getAddress(), endpoint.getPort())); +// PendingRangeCalculatorService.instance.update(); +// PendingRangeCalculatorService.instance.blockUntilFinished(); +// }).accept(cluster.get(2).broadcastAddress()); // prevent #4 from reading or writing to #3, so our QUORUM must contain #2 and #4 // since #1 is taking over the range, this means any read-repair must make it to #1 as well @@ -350,9 +352,9 @@ public class ReadRepairTest extends TestBaseImpl } @Test - public void readRepairRTRangeMovementTest() throws Throwable + public void readRepairRTRangeMovementTest() throws IOException { - ExecutorService es = Executors.newFixedThreadPool(1); + ExecutorPlus es = ExecutorFactory.Global.executorFactory().sequential("query-executor"); String key = "test1"; try (Cluster cluster = init(Cluster.build() .withConfig(config -> config.with(Feature.GOSSIP, Feature.NETWORK) @@ -395,9 +397,20 @@ public class ReadRepairTest extends TestBaseImpl } return false; }).drop(); - Future read = es.submit(() -> cluster.coordinator(3) - .execute("SELECT * FROM distributed_test_keyspace.tbl WHERE key=? and column1 >= ? and column1 <= ?", - ALL, key, 20, 40)); + + String query = "SELECT * FROM distributed_test_keyspace.tbl WHERE key=? and column1 >= ? and column1 <= ?"; + Future read = es.submit(() -> cluster.coordinator(3).execute(query, ALL, key, 20, 40)); + read.addCallback(new FutureCallback() + { + @Override + public void onSuccess(Object @Nullable [][] objects) + { + Assert.fail("Expected read failure because replica placements have become incompatible during execution"); + } + + @Override + public void onFailure(Throwable t) {} + }); readStarted.await(); IInstanceConfig config = cluster.newInstanceConfig(); config.set("auto_bootstrap", true); @@ -405,6 +418,16 @@ public class ReadRepairTest extends TestBaseImpl continueRead.signalAll(); read.get(); } + catch (ExecutionException e) + { + Throwable cause = e.getCause(); + Assert.assertTrue("Expected a different error message, but got " + cause.getMessage(), + cause.getMessage().contains("Operation failed - received 1 responses and 1 failures: INVALID_ROUTING from /127.0.0.2:7012")); + } + catch (InterruptedException e) + { + Assert.fail("Unexpected exception"); + } finally { es.shutdown(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/RemoveNodeTest.java b/test/distributed/org/apache/cassandra/distributed/test/RemoveNodeTest.java new file mode 100644 index 0000000000..baa7ae4c6d --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/RemoveNodeTest.java @@ -0,0 +1,99 @@ +/* + * 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.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.sequences.SingleNodeSequences; + +import static org.junit.Assert.assertTrue; + +public class RemoveNodeTest extends TestBaseImpl +{ + @Test + public void testAbort() throws Exception + { + try (Cluster cluster = init(Cluster.build(3) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + cluster.filters().inbound().verbs(Verb.INITIATE_DATA_MOVEMENTS_REQ.id).drop(); + String nodeId = cluster.get(3).callOnInstance(() -> ClusterMetadata.current().myNodeId().toUUID().toString()); + cluster.get(3).shutdown().get(); + Thread t = new Thread(() -> cluster.get(1).nodetoolResult("removenode", nodeId, "--force")); + t.start(); + cluster.get(1).logs().watchFor("Committed StartLeave"); + String stdout = cluster.get(1).nodetoolResult("removenode", "status").getStdout(); + assertTrue(String.format("\"%s\" does not contain MID_LEAVE", stdout), stdout.contains("MID_LEAVE")); + + cluster.get(1).nodetoolResult("removenode", "abort", nodeId).asserts().success(); + cluster.get(2).logs().watchFor("Enacted CancelInProgressSequence"); + + + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + assertTrue(metadata.inProgressSequences.isEmpty()); + assertTrue(metadata.directory.peerState(NodeId.fromString(nodeId)) == NodeState.JOINED); + }); + + } + } + + @Test + public void removeNodeWithNoStreamingRequired() throws Exception + { + // 2 node cluster, keyspaces have RF=2. If we removenode(node2), then no outbound streams are created + // as all the data is already fully replicated to node1. In this case, ensure that RemoveNodeStreams + // doesn't hang indefinitely. + try (Cluster cluster = init(Cluster.build(2) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(2, "dc0", "rack0")) + .withConfig(config -> config.set("default_keyspace_rf", "2") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + int toRemove = cluster.get(2).callOnInstance(() -> ClusterMetadata.current().myNodeId().id()); + cluster.get(2).shutdown(false).get(); + + cluster.get(1).runOnInstance(() -> { + NodeId nodeId = new NodeId(toRemove); + InetAddressAndPort endpoint = ClusterMetadata.current().directory.endpoint(nodeId); + FailureDetector.instance.forceConviction(endpoint); + SingleNodeSequences.removeNode(nodeId, true); + }); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + assertTrue(metadata.inProgressSequences.isEmpty()); + assertTrue(metadata.directory.peerState(new NodeId(toRemove)) == NodeState.LEFT); + }); + } + + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorFast.java b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorFast.java index 0e156dab98..c80ba98286 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorFast.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorFast.java @@ -53,10 +53,12 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase super(repairType, parallelism, withNotifications); } + private static Duration TIMEOUT = Duration.ofMinutes(2); + @Test public void simple() { String table = tableName("simple"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, PRIMARY KEY (key))", KEYSPACE, table)); CLUSTER.coordinator(1).execute(format("INSERT INTO %s.%s (key) VALUES (?)", KEYSPACE, table), ConsistencyLevel.ANY, "some text"); @@ -88,7 +90,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase @Test public void missingKeyspace() { - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { // as of this moment the check is done in nodetool so the JMX notifications are not imporant // nor is the history stored long repairExceptions = getRepairExceptions(CLUSTER, 2); @@ -106,7 +108,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase @Test public void missingTable() { - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { long repairExceptions = getRepairExceptions(CLUSTER, 2); String tableName = tableName("doesnotexist"); NodeToolResult result = repair(2, KEYSPACE, tableName); @@ -134,7 +136,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase // this is done in this test to cause the keyspace to have 0 tables to repair, which causes repair to no-op // early and skip. String table = tableName("withindex"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); CLUSTER.schemaChange(format("CREATE INDEX value_%s ON %s.%s (value)", postfix(), KEYSPACE, table)); @@ -167,7 +169,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase // repair to fail but it didn't, this would be fine and this test should be updated to reflect the new // semantic String table = tableName("intersectingrange"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); //TODO dtest api for this? @@ -206,7 +208,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase public void unknownHost() { String table = tableName("unknownhost"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); long repairExceptions = getRepairExceptions(CLUSTER, 2); @@ -264,7 +266,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase // this is very similar to ::desiredHostNotCoordinator but has the difference that the only host to do repair // is the coordinator String table = tableName("onlycoordinator"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); long repairExceptions = getRepairExceptions(CLUSTER, 2); @@ -293,7 +295,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase { // In the case of rf=1 repair fails to create a cmd handle so node tool exists early String table = tableName("one"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { // since cluster is shared and this test gets called multiple times, need "IF NOT EXISTS" so the second+ attempt // does not fail CLUSTER.schemaChange("CREATE KEYSPACE IF NOT EXISTS replicationfactor WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); @@ -314,7 +316,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase public void prepareFailure() { String table = tableName("preparefailure"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); IMessageFilters.Filter filter = CLUSTER.verbs(Verb.PREPARE_MSG).messagesMatching(of(m -> { throw new RuntimeException("prepare fail"); @@ -359,7 +361,7 @@ public abstract class RepairCoordinatorFast extends RepairCoordinatorBase Assume.assumeFalse("Parallel repair does not perform snapshots", parallelism == RepairParallelism.PARALLEL); String table = tableName("snapshotfailure"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(TIMEOUT, () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); IMessageFilters.Filter filter = CLUSTER.verbs(Verb.SNAPSHOT_MSG).messagesMatching(of(m -> { throw new RuntimeException("snapshot fail"); diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java index 4228806fa7..7815cbe3bc 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairCoordinatorNeighbourDown.java @@ -31,14 +31,17 @@ import org.junit.Before; import org.junit.Test; import org.apache.cassandra.distributed.api.NodeToolResult; +import org.apache.cassandra.distributed.shared.WithProperties; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairParallelism; import org.apache.cassandra.distributed.test.DistributedRepairUtils.RepairType; import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import static java.lang.String.format; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY; import static org.apache.cassandra.distributed.api.IMessageFilters.Matcher.of; import static org.apache.cassandra.distributed.test.DistributedRepairUtils.assertParentRepairFailedWithMessageContains; import static org.apache.cassandra.distributed.test.DistributedRepairUtils.assertParentRepairNotExist; @@ -60,22 +63,28 @@ public abstract class RepairCoordinatorNeighbourDown extends RepairCoordinatorBa try { i.startup(); + i.runOnInstance(StorageService.instance::disableAutoCompaction); } catch (IllegalStateException e) { // ignore, node wasn't down } }); + } @Test public void neighbourDown() { String table = tableName("neighbourdown"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(Duration.ofMinutes(2), () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); String downNodeAddress = CLUSTER.get(2).callOnInstance(() -> FBUtilities.getBroadcastAddressAndPort().getHostAddressAndPort()); - Future shutdownFuture = CLUSTER.get(2).shutdown(); + Future shutdownFuture; + try(WithProperties ignore_ = new WithProperties().set(TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY, "true")) + { + shutdownFuture = CLUSTER.get(2).shutdown(); + } try { // wait for the node to stop @@ -135,19 +144,24 @@ public abstract class RepairCoordinatorNeighbourDown extends RepairCoordinatorBa // Currently this isn't recoverable but could be. // TODO since this is a real restart, how would I test "long pause"? Can't send SIGSTOP since same procress String table = tableName("validationparticipentcrashesandcomesback"); - assertTimeoutPreemptively(Duration.ofMinutes(1), () -> { + assertTimeoutPreemptively(Duration.ofMinutes(2), () -> { CLUSTER.schemaChange(format("CREATE TABLE %s.%s (key text, value text, PRIMARY KEY (key))", KEYSPACE, table)); AtomicReference> participantShutdown = new AtomicReference<>(); CLUSTER.verbs(Verb.VALIDATION_REQ).to(2).messagesMatching(of(m -> { // the nice thing about this is that this lambda is "capturing" and not "transfer", what this means is that // this lambda isn't serialized and any object held isn't copied. - participantShutdown.set(CLUSTER.get(2).shutdown()); + Future shutdownFuture; + try(WithProperties ignore_ = new WithProperties().set(TEST_JVM_SHUTDOWN_MESSAGING_GRACEFULLY, "true")) + { + shutdownFuture = CLUSTER.get(2).shutdown(); + } + participantShutdown.set(shutdownFuture); return true; // drop it so this node doesn't reply before shutdown. })).drop(); // since nodetool is blocking, need to handle participantShutdown in the background CompletableFuture recovered = CompletableFuture.runAsync(() -> { try { - while (participantShutdown.get() == null) { + while (participantShutdown.get() == null && !Thread.interrupted()) { // event not happened, wait for it TimeUnit.MILLISECONDS.sleep(100); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java b/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java index b702855f68..1c50f0ee3b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairDigestTrackingTest.java @@ -30,20 +30,29 @@ import java.util.stream.Stream; import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Assert; +import org.apache.cassandra.concurrent.SEPExecutor; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.ReplicaLayout; +import org.apache.cassandra.locator.ReplicaUtils; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.Throwables; import org.junit.Test; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.SuperCall; -import org.apache.cassandra.concurrent.SEPExecutor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInvokableInstance; @@ -52,16 +61,10 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.StatsComponent; import org.apache.cassandra.io.sstable.metadata.StatsMetadata; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.ReplicaLayout; -import org.apache.cassandra.locator.ReplicaUtils; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageProxy.LocalReadRunnable; import org.apache.cassandra.utils.DiagnosticSnapshotService; -import org.apache.cassandra.utils.Throwables; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @@ -84,10 +87,9 @@ public class RepairDigestTrackingTest extends TestBaseImpl { try (Cluster cluster = init(builder().withNodes(2).start())) { - cluster.get(1).runOnInstance(() -> StorageProxy.instance.enableRepairedDataTrackingForRangeReads()); - cluster.schemaChange("CREATE TABLE " + KS_TABLE+ " (k INT, c INT, v INT, PRIMARY KEY (k,c)) with read_repair='NONE'"); + setupSchema(cluster, "CREATE TABLE " + KS_TABLE+ " (k INT, c INT, v INT, PRIMARY KEY (k,c)) with read_repair='NONE' AND compaction = {'class':'SizeTieredCompactionStrategy'};"); for (int i = 0; i < 10; i++) { cluster.coordinator(1).execute("INSERT INTO " + KS_TABLE + " (k, c, v) VALUES (?, ?, ?)", @@ -128,7 +130,7 @@ public class RepairDigestTrackingTest extends TestBaseImpl { cluster.get(1).runOnInstance(() -> StorageProxy.instance.enableRepairedDataTrackingForRangeReads()); - cluster.schemaChange("CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, v2 INT, PRIMARY KEY (k,c)) WITH gc_grace_seconds=0"); + setupSchema(cluster, "CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, v2 INT, PRIMARY KEY (k,c)) WITH gc_grace_seconds=0"); // on node1 only insert some tombstones, then flush for (int i = 0; i < 10; i++) { @@ -171,11 +173,14 @@ public class RepairDigestTrackingTest extends TestBaseImpl @Test public void testSnapshottingOnInconsistency() throws Throwable { - try (Cluster cluster = init(Cluster.create(2))) + try (WithProperties ignore_ = new WithProperties() + .set(CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR, "true"); + Cluster cluster = init(Cluster.create(2))) { cluster.get(1).runOnInstance(() -> StorageProxy.instance.enableRepairedDataTrackingForPartitionReads()); - cluster.schemaChange("CREATE TABLE " + KS_TABLE + " (k INT, c INT, v INT, PRIMARY KEY (k,c))"); + setupSchema(cluster, "CREATE TABLE " + KS_TABLE + " (k INT, c INT, v INT, PRIMARY KEY (k,c))"); + for (int i = 0; i < 10; i++) { cluster.coordinator(1).execute("INSERT INTO " + KS_TABLE + " (k, c, v) VALUES (0, ?, ?)", @@ -228,13 +233,12 @@ public class RepairDigestTrackingTest extends TestBaseImpl // limits of the read request. try (Cluster cluster = init(Cluster.create(2))) { - cluster.get(1).runOnInstance(() -> { StorageProxy.instance.enableRepairedDataTrackingForRangeReads(); StorageProxy.instance.enableRepairedDataTrackingForPartitionReads(); }); - cluster.schemaChange("CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, PRIMARY KEY (k,c)) " + + setupSchema(cluster, "CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, PRIMARY KEY (k,c)) " + "WITH CLUSTERING ORDER BY (c DESC)"); // insert data on both nodes and flush @@ -291,17 +295,16 @@ public class RepairDigestTrackingTest extends TestBaseImpl // Asserts that the amount of repaired data read for digest generation is consistent // across replicas where one has to read more repaired data to satisfy the original // limits of the read request. - try (Cluster cluster = init(Cluster.create(2))) + try (WithProperties ignore_ = new WithProperties().set(CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR, "true"); + Cluster cluster = init(Cluster.create(2))) { - + setupSchema(cluster,"CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, PRIMARY KEY (k,c)) " + + "WITH CLUSTERING ORDER BY (c DESC)"); cluster.get(1).runOnInstance(() -> { StorageProxy.instance.enableRepairedDataTrackingForRangeReads(); StorageProxy.instance.enableRepairedDataTrackingForPartitionReads(); }); - cluster.schemaChange("CREATE TABLE " + KS_TABLE + " (k INT, c INT, v1 INT, PRIMARY KEY (k,c)) " + - "WITH CLUSTERING ORDER BY (c DESC)"); - // insert data on both nodes and flush for (int i=0; i<10; i++) { @@ -379,7 +382,9 @@ public class RepairDigestTrackingTest extends TestBaseImpl @Test public void testLocalDataAndRemoteRequestConcurrency() throws Exception { - try (Cluster cluster = init(Cluster.build(3) + + try (WithProperties ignore_ = new WithProperties().set(CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR, "true"); + Cluster cluster = init(Cluster.build(3) .withInstanceInitializer(BBHelper::install) .withConfig(config -> config.set("repaired_data_tracking_for_partition_reads_enabled", true) .with(GOSSIP) @@ -432,7 +437,7 @@ public class RepairDigestTrackingTest extends TestBaseImpl .load(classLoader, ClassLoadingStrategy.Default.INJECTION); new ByteBuddy().rebase(ReplicaLayout.class) - .method(named("forTokenReadLiveSorted").and(takesArguments(AbstractReplicationStrategy.class, Token.class))) + .method(named("forTokenReadLiveSorted").and(takesArguments(ClusterMetadata.class, Keyspace.class, AbstractReplicationStrategy.class, Token.class))) .intercept(MethodDelegation.to(BBHelper.class)) .make() .load(classLoader, ClassLoadingStrategy.Default.INJECTION); @@ -467,7 +472,7 @@ public class RepairDigestTrackingTest extends TestBaseImpl } @SuppressWarnings({ "unused" }) - public static ReplicaLayout.ForTokenRead forTokenReadLiveSorted(AbstractReplicationStrategy replicationStrategy, Token token) + public static ReplicaLayout.ForTokenRead forTokenReadLiveSorted(ClusterMetadata metadata, Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, Token token) { try { @@ -618,7 +623,8 @@ public class RepairDigestTrackingTest extends TestBaseImpl { cluster.schemaChange(cql); // disable auto compaction to prevent nodes from trying to compact - // new sstables with ones we've modified to mark repaired + // new sstables with ones we've modified to mark repaired, as this may lead to races + // where repaired digests are goingto be empty, drawing this test meaningless. cluster.forEach(i -> i.runOnInstance(() -> Keyspace.open(KEYSPACE) .getColumnFamilyStore(TABLE) .disableAutoCompaction())); diff --git a/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java b/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java index 537599c297..7f2ef5ca01 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/RepairErrorsTest.java @@ -89,13 +89,21 @@ public class RepairErrorsTest extends TestBaseImpl @Test public void testRemoteSyncFailure() throws Exception { - try (Cluster cluster = init(Cluster.build(3) + try (Cluster cluster = Cluster.build(3) .withConfig(config -> config.with(GOSSIP) .with(NETWORK) .set("disk_failure_policy", "stop") .set("disk_access_mode", "mmap_index_only")) - .withInstanceInitializer(ByteBuddyHelper::installStreamPlanExecutionFailure).start())) + .withInstanceInitializer(ByteBuddyHelper::installStreamPlanExecutionFailure) + .createWithoutStarting()) { + // This test relies on the fact that 2->3 streaming is going to fail, but if we're using vnodes, + // 2 will effectively become 3 because of the token allocator. To avoid this, we simply start the nodes sequentially + // and guarantee their tokens order. + for (int i = 1; i <= 3; i++) + cluster.get(i).startup(); + + init(cluster); cluster.schemaChange("create table " + KEYSPACE + ".tbl (id int primary key, x int)"); // On repair, this data layout will require two (local) syncs from node 1 and one remote sync from node 2: diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java index 62f6139b34..9514faf9ea 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableIdGenerationTest.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; import org.apache.commons.io.FileUtils; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -83,6 +84,11 @@ public class SSTableIdGenerationTest extends TestBaseImpl TestBaseImpl.beforeClass(); originalSecurityManager = System.getSecurityManager(); + } + + @Before + public void beforeEach() + { // we prevent system exit and convert it to exception becuase this is one of the expected test outcomes, // and we want to make an assertion on that ClusterUtils.preventSystemExit(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java b/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java index 142b66b581..7a09d94fdf 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SchemaTest.java @@ -18,28 +18,28 @@ package org.apache.cassandra.distributed.test; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.Callable; import org.junit.Test; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableCallable; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaConstants; -import org.assertj.core.api.Assertions; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.AlterSchema; import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; import static java.time.Duration.ofSeconds; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseAfterEnacting; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeEnacting; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseEnactment; import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class SchemaTest extends TestBaseImpl @@ -54,15 +54,40 @@ public class SchemaTest extends TestBaseImpl { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, primary key (pk, ck))"); String name = "aaa"; - cluster.get(1).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); - cluster.get(1).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2) values (?,1,1,1)", 1); - selectSilent(cluster, name); + // have the CMS node pause directly before committing the ALTER TABLE so we can infer the next epoch + Callable beforeCommit = pauseBeforeCommit(cluster.get(1), (e) -> e instanceof AlterSchema); + new Thread(() -> { + cluster.get(1).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); + cluster.get(1).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2) values (?,1,1,1)", 1); + }).start(); + + Epoch targetEpoch = beforeCommit.call().nextEpoch(); + // pause the replica immediately before and after enacting the ALTER TABLE stmt + Callable beforeEnactedOnReplica = pauseBeforeEnacting(cluster.get(2), targetEpoch); + Callable afterEnactedOnReplica = pauseAfterEnacting(cluster.get(2), targetEpoch); + // unpause the CMS node and allow it to commit and replicate the ALTER TABLE + unpauseCommits(cluster.get(1)); + + // Wait for the replica to signal that it has paused before enacting the schema change + // then execute the query and assert that a schema disagreement error was triggered + beforeEnactedOnReplica.call(); + selectExpectingError(cluster, name); + + // unpause the replica and wait until it notifies that it has enacted the schema change + unpauseEnactment(cluster.get(2)); + afterEnactedOnReplica.call(); + unpauseEnactment(cluster.get(2)); cluster.get(2).flush(KEYSPACE); - cluster.get(2).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); + // now that the replica has enacted the alter table, an attempt to repeat it should be rejected + alterTableExpectingError(cluster.get(2), name); + // bouncing the replica should be safe as SSTables aren't loaded until the log replay is complete + // and the schema is in it's most up to date state cluster.get(2).shutdown().get(); cluster.get(2).startup(); + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ?"), ConsistencyLevel.ALL, 1); cluster.get(2).forceCompact(KEYSPACE, "tbl"); + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ?"), ConsistencyLevel.ALL, 1); } } @@ -73,21 +98,50 @@ public class SchemaTest extends TestBaseImpl { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, primary key (pk, ck))"); String name = "v10"; - cluster.get(1).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); - cluster.get(1).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2) values (?,1,1,1)", 1); - selectSilent(cluster, name); + + // have the CMS node pause directly before committing the ALTER TABLE so we can infer the next epoch + Callable beforeCommit = pauseBeforeCommit(cluster.get(1), (e) -> e instanceof AlterSchema); + new Thread(() -> { + cluster.get(1).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); + cluster.get(1).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2) values (?,1,1,1)", 1); + }).start(); + Epoch targetEpoch = beforeCommit.call().nextEpoch(); + + // pause the replica immediately before and after enacting the ALTER TABLE stmt + Callable beforeEnactedOnReplica = pauseBeforeEnacting(cluster.get(2), targetEpoch); + Callable afterEnactedOnReplica = pauseAfterEnacting(cluster.get(2), targetEpoch); + // unpause the CMS node and allow it to commit and replicate the ALTER TABLE + unpauseCommits(cluster.get(1)); + + // Wait for the replica to signal that it has paused before enacting the schema change + // then execute the query and assert that a schema disagreement error was triggered + beforeEnactedOnReplica.call(); + selectExpectingError(cluster, name); + + // unpause the replica and wait until it notifies that it has enacted the schema change + unpauseEnactment(cluster.get(2)); + afterEnactedOnReplica.call(); + unpauseEnactment(cluster.get(2)); + cluster.get(2).flush(KEYSPACE); - cluster.get(2).schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); + // now that the replica has enacted the alter table, an attempt to repeat it should be rejected + alterTableExpectingError(cluster.get(2), name); cluster.get(2).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1, v2, " + name + ") values (?,1,1,1,[1])", 1); cluster.get(2).flush(KEYSPACE); cluster.get(2).forceCompact(KEYSPACE, "tbl"); + + // bouncing the replica should be safe as SSTables aren't loaded until the log replay is complete + // and the schema is in it's most up to date state cluster.get(2).shutdown().get(); cluster.get(2).startup(); + + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ?"), ConsistencyLevel.ALL, 1); cluster.get(2).forceCompact(KEYSPACE, "tbl"); + cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.tbl WHERE pk = ?"), ConsistencyLevel.ALL, 1); } } - private void selectSilent(Cluster cluster, String name) + private void selectExpectingError(Cluster cluster, String name) { try { @@ -107,29 +161,43 @@ public class SchemaTest extends TestBaseImpl } } + private void alterTableExpectingError(IInvokableInstance instance, String name) + { + try + { + instance.schemaChangeInternal("ALTER TABLE " + KEYSPACE + ".tbl ADD " + name + " list"); + } + catch (Exception e) + { + boolean causeIsColumnExists = false; + Throwable cause = e; + while (cause != null) + { + if (cause.getMessage() != null && cause.getMessage().contains("Column with name '" + name + "' already exists")) + causeIsColumnExists = true; + cause = cause.getCause(); + } + assertTrue(causeIsColumnExists); + } + } + /** - * The purpose of this test is to verify manual schema reset functinality. + * The original purpose of this test was to verify manual schema reset functionality, but with schema updates being + * serialized in the cluster metadata log local schema reset no longer makes sense so the assertions have been + * modified to verify that schema changes are correctly propagated to down nodes once they come back up. *

    * There is a 2-node cluster and a TABLE_ONE created. The schema version is agreed on both nodes. Then the 2nd node * is shutdown. We introduce a disagreement by dropping TABLE_ONE and creating TABLE_TWO on the 1st node. Therefore, * the 1st node has a newer schema version with TABLE_TWO, while the shutdown 2nd node has older schema version with * TABLE_ONE. *

    - * At this point, if we just started the 2nd node, it would sync its schema by getting fresh mutations from the 1st - * node which would result in both nodes having only the definition of TABLE_TWO. + * At this point, if we just start the 2nd node, it would sync its schema by getting the transformations that it + * missed while down, which would result in both nodes having only the definition of TABLE_TWO. *

    - * However, before starting the 2nd node the schema is reset on the 1st node, so the 1st node will discard its local - * schema whenever it manages to fetch a schema definition from some other node (the 2nd node in this case). - * It is expected to end up with both nodes having only the definition of TABLE_ONE. - *

    - * In the second phase of the test we simply break the schema on the 1st node and call reset to fetch the schema - * definition it from the 2nd node. */ @Test - public void schemaReset() throws Throwable + public void schemaPropagationToDownNode() throws Throwable { - CassandraRelevantProperties.MIGRATION_DELAY.setLong(10000); - CassandraRelevantProperties.SCHEMA_PULL_INTERVAL_MS.setLong(10000); try (Cluster cluster = init(Cluster.build(2).withConfig(cfg -> cfg.with(Feature.GOSSIP, Feature.NETWORK)).start())) { // create TABLE_ONE and make sure it is propagated @@ -139,49 +207,22 @@ public class SchemaTest extends TestBaseImpl // shutdown the 2nd node and make sure that the 1st does not see it any longer as alive cluster.get(2).shutdown().get(); - await(30).until(() -> cluster.get(1).callOnInstance(() -> { + await(60).until(() -> cluster.get(1).callOnInstance(() -> { return Gossiper.instance.getLiveMembers() .stream() .allMatch(e -> e.equals(getBroadcastAddressAndPort())); })); - // when there is no node to fetch the schema from, reset local schema should immediately fail - Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> { - cluster.get(1).runOnInstance(() -> Schema.instance.resetLocalSchema()); - }).withMessageContaining("Cannot reset local schema when there are no other live nodes"); - // now, let's make a disagreement, the shutdown node 2 has a definition of TABLE_ONE, while the running // node 1 will have a definition of TABLE_TWO cluster.coordinator(1).execute(String.format("DROP TABLE %s.%s", KEYSPACE, TABLE_ONE), ConsistencyLevel.ONE); cluster.coordinator(1).execute(String.format("CREATE TABLE %s.%s (pk INT PRIMARY KEY, v TEXT)", KEYSPACE, TABLE_TWO), ConsistencyLevel.ONE); - await(30).until(() -> checkTablesPropagated(cluster.get(1), false, true)); - - // Schema.resetLocalSchema is guarded by some conditions which would not let us reset schema if there is no - // live node in the cluster, therefore we simply call SchemaUpdateHandler.clear (this is the only real thing - // being done by Schema.resetLocalSchema under the hood) - SerializableCallable clear = () -> Schema.instance.updateHandler.clear().awaitUninterruptibly(1, TimeUnit.MINUTES); - Future clear1 = cluster.get(1).asyncCallsOnInstance(clear).call(); - assertFalse(clear1.isDone()); + await(60).until(() -> checkTablesPropagated(cluster.get(1), false, true)); // when the 2nd node is started, schema should be back in sync cluster.get(2).startup(); - await(30).until(() -> clear1.isDone() && clear1.get()); - - // this proves that reset schema works on the 1st node - the most recent change should be discarded because - // it receives the schema from the 2nd node and applies it on empty schema - await(60).until(() -> checkTablesPropagated(cluster.get(1), true, false)); - - // now let's break schema locally and let it be reset - cluster.get(1).runOnInstance(() -> Schema.instance.getLocalKeyspaces() - .get(SchemaConstants.SCHEMA_KEYSPACE_NAME) - .get().tables.forEach(t -> ColumnFamilyStore.getIfExists(t.keyspace, t.name).truncateBlockingWithoutSnapshot())); - - // when schema is removed and there is a node to fetch it from, the 1st node should immediately restore it - cluster.get(1).runOnInstance(() -> Schema.instance.resetLocalSchema()); - // note that we should not wait for this to be true because resetLocalSchema is blocking - // and after successfully completing it, the schema should be already back in sync - assertTrue(checkTablesPropagated(cluster.get(1), true, false)); - assertTrue(checkTablesPropagated(cluster.get(2), true, false)); + assertTrue(checkTablesPropagated(cluster.get(1), false, true)); + assertTrue(checkTablesPropagated(cluster.get(2), false, true)); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexTest.java b/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexTest.java index 3b55dcf27d..8d32a6c042 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SecondaryIndexTest.java @@ -29,7 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.awaitility.Awaitility; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; @@ -40,6 +39,7 @@ import org.junit.Test; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.utils.TimeUUID; +import org.awaitility.Awaitility; public class SecondaryIndexTest extends TestBaseImpl { diff --git a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java index 35c59046ea..1d8970b683 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java @@ -31,22 +31,39 @@ import java.util.function.Function; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; - import org.junit.After; import org.junit.BeforeClass; import org.apache.cassandra.cql3.Duration; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.ByteType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.DecimalType; +import org.apache.cassandra.db.marshal.DoubleType; +import org.apache.cassandra.db.marshal.DurationType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.InetAddressType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.ShortType; +import org.apache.cassandra.db.marshal.TimestampType; +import org.apache.cassandra.db.marshal.TupleType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.shared.DistributedTestBase; -import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SCHEMA_DELAY_MS; import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; +import static org.apache.cassandra.config.CassandraRelevantProperties.RESET_BOOTSTRAP_PROGRESS; +import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_GC_INSPECTOR; import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; +// checkstyle: suppress below 'blockSystemPropertyUsage' public class TestBaseImpl extends DistributedTestBase { public static final Object[][] EMPTY_ROWS = new Object[0][]; @@ -61,6 +78,8 @@ public class TestBaseImpl extends DistributedTestBase public static void beforeClass() throws Throwable { ICluster.setup(); + SKIP_GC_INSPECTOR.setBoolean(true); + System.setProperty("sigar.nativeLogging", "false"); } @Override @@ -122,9 +141,11 @@ public class TestBaseImpl extends DistributedTestBase IInstanceConfig config = cluster.newInstanceConfig(); config.set("auto_bootstrap", true); IInvokableInstance newInstance = cluster.bootstrap(config); - withProperty(BOOTSTRAP_SCHEMA_DELAY_MS, Integer.toString(90 * 1000), - () -> withProperty(JOIN_RING, false, () -> newInstance.startup(cluster))); + RESET_BOOTSTRAP_PROGRESS.setBoolean(false); + withProperty(JOIN_RING, false, + () -> newInstance.startup(cluster)); newInstance.nodetoolResult("join").asserts().success(); + newInstance.nodetoolResult("describecms").asserts().success(); // just make sure we're joined, remove later } @SuppressWarnings("unchecked") diff --git a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java new file mode 100644 index 0000000000..238fca419c --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java @@ -0,0 +1,134 @@ +/* + * 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 java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.utils.Pair; + +import static com.google.common.collect.Lists.newArrayList; +import static org.apache.cassandra.distributed.test.TransientRangeMovementTest.OPPTokens; +import static org.apache.cassandra.distributed.test.TransientRangeMovementTest.assertAllContained; +import static org.apache.cassandra.distributed.test.TransientRangeMovementTest.localStrs; +import static org.apache.cassandra.distributed.test.TransientRangeMovementTest.populate; + +@SuppressWarnings("unchecked") +public class TransientRangeMovement2Test extends TestBaseImpl +{ + @Test + public void testMoveBackward() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(Cluster.build(4) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") // just makes it easier to read the tokens in the log + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + cluster.get(3).nodetoolResult("move", "25").asserts().success(); + cluster.forEach(i -> i.nodetoolResult("cleanup").asserts().success()); + + assertAllContained(localStrs(cluster.get(1)), + newArrayList("22", "24"), + Pair.create("00", "10"), + Pair.create("26", "50")); + assertAllContained(localStrs(cluster.get(2)), + newArrayList("26", "28", "30", "32", "34", "36", "38", "40"), + Pair.create("00", "20"), + Pair.create("41", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("42", "44", "46", "48", "00", "02", "04", "06", "08", "10"), + Pair.create("11", "25")); + assertAllContained(localStrs(cluster.get(4)), + newArrayList("12", "14", "16", "18", "20"), + Pair.create("21", "40")); + } + } + + @Test + public void testMoveForward() throws IOException, ExecutionException, InterruptedException + { + + try (Cluster cluster = init(Cluster.build(4) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") // just makes it easier to read the tokens in the log + .set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + cluster.get(1).nodetoolResult("move", "15").asserts().success(); + cluster.forEach((i) -> i.nodetoolResult("cleanup").asserts().success()); + assertAllContained(localStrs(cluster.get(1)), + newArrayList("22", "24", "26", "28", "30"), + Pair.create("00", "15"), + Pair.create("31", "50")); + assertAllContained(localStrs(cluster.get(2)), + newArrayList("32", "34", "36", "38", "40"), + Pair.create("00", "20"), + Pair.create("41", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("42", "44", "46", "48", "00", "02", "04", "06", "08", "10", "12", "14"), + Pair.create("16", "30")); + assertAllContained(localStrs(cluster.get(4)), + newArrayList("16", "18", "20"), + Pair.create("21", "40")); + } + } + + + + @Test + public void testRebuild() throws ExecutionException, InterruptedException, IOException + { + try (Cluster cluster = init(Cluster.build(3) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") + .set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + IInstanceConfig config = cluster.newInstanceConfig(); + config.set("auto_bootstrap", false); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(); + cluster.forEach(i -> i.nodetoolResult("cleanup").asserts().success()); + cluster.get(4).nodetoolResult("rebuild", "-ks", "tr", "--tokens", "(15, 18],(20,25]").asserts().success(); + assertAllContained(localStrs(cluster.get(4)), + newArrayList(), + Pair.create("16", "18"), + Pair.create("21", "25")); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java new file mode 100644 index 0000000000..c1236cc2ad --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java @@ -0,0 +1,272 @@ +/* + * 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 java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import com.google.common.collect.Sets; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.Pair; + +import static com.google.common.collect.Lists.newArrayList; +import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK; +import static org.apache.cassandra.distributed.shared.ClusterUtils.awaitRingJoin; +import static org.apache.cassandra.distributed.shared.ClusterUtils.replaceHostAndStart; +import static org.apache.cassandra.distributed.shared.ClusterUtils.stopUnchecked; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings("unchecked") +public class TransientRangeMovementTest extends TestBaseImpl +{ + @Test + public void testBootstrap() throws IOException, ExecutionException, InterruptedException + { + + try (Cluster cluster = init(Cluster.build(3) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") + .set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + IInstanceConfig config = cluster.newInstanceConfig(); + config.set("auto_bootstrap", true); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(); + cluster.forEach(i -> i.nodetoolResult("cleanup").asserts().success()); + + assertAllContained(localStrs(cluster.get(1)), + newArrayList("22", "24", "26", "28", "30"), + Pair.create("00", "10"), + Pair.create("31", "50")); + assertAllContained(localStrs(cluster.get(2)), + newArrayList("32", "34", "36", "38", "40"), + Pair.create("00", "20"), + Pair.create("41", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("00", "02", "04", "06", "08", "10", "42", "44", "46", "48"), + Pair.create("11", "30")); + assertAllContained(localStrs(cluster.get(4)), + newArrayList("10", "12", "14", "16", "18", "20"), + Pair.create("21", "40")); + } + } + + @Test + public void testReplace() throws IOException, ExecutionException, InterruptedException + { + + try (Cluster cluster = init(Cluster.build(3) + .withTokenSupplier(new OPPTokensReplace()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + stopUnchecked(cluster.get(2)); + IInvokableInstance replacingNode = replaceHostAndStart(cluster, cluster.get(2), props -> { + // since we have a downed host there might be a schema version which is old show up but + // can't be fetched since the host is down... + props.set(BOOTSTRAP_SKIP_SCHEMA_CHECK, true); + }); + awaitRingJoin(cluster.get(1), replacingNode); + awaitRingJoin(replacingNode, cluster.get(1)); + cluster.forEach(i -> { if (i.config().num() != 2) i.nodetoolResult("cleanup").asserts().success();} ); + assertAllContained(localStrs(cluster.get(1)), + newArrayList("12", "14", "16", "18", "20"), + Pair.create("00", "10"), + Pair.create("21", "50")); + assertAllContained(localStrs(cluster.get(4)), + newArrayList("22", "24", "26", "28", "30"), + Pair.create("00", "20"), + Pair.create("31", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("00", "02", "04", "06", "08", "10", + "32", "34", "36", "38", "40", "42", "44", "46", "48"), + Pair.create("11", "30")); + } + } + + @Test + public void testLeave() throws IOException, ExecutionException, InterruptedException + { + + try (Cluster cluster = init(Cluster.build(4) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") // just makes it easier to read the tokens in the log + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + cluster.get(4).nodetoolResult("decommission", "--force").asserts().success(); + cluster.forEach(i -> i.nodetoolResult("cleanup")); + assertAllContained(localStrs(cluster.get(1)), + newArrayList("12", "14", "16", "18", "20"), + Pair.create("00", "10"), + Pair.create("21", "50")); + assertAllContained(localStrs(cluster.get(2)), + newArrayList("22", "24", "26", "28", "30"), + Pair.create("00", "20"), + Pair.create("31", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("00", "02", "04", "06", "08", "10", + "32", "34", "36", "38", "40", "42", "44", "46", "48"), + Pair.create("11", "30")); + } + } + + @Test + public void testRemoveNode() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(Cluster.build(4) + .withTokenSupplier(new OPPTokens()) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("partitioner", "OrderPreservingPartitioner") + .set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start())) + { + populate(cluster); + String nodeId = cluster.get(4).callOnInstance(() -> ClusterMetadata.current().myNodeId().toUUID().toString()); + cluster.get(4).shutdown().get(); + cluster.get(1).nodetoolResult("removenode", nodeId, "--force").asserts().success(); + cluster.forEach(i -> { + if (i.config().num() != 4) + i.nodetoolResult("cleanup").asserts().success(); + }); + + assertAllContained(localStrs(cluster.get(1)), + newArrayList("12", "14", "16", "18", "20"), + Pair.create("00", "10"), + Pair.create("21", "50")); + assertAllContained(localStrs(cluster.get(2)), + newArrayList("22", "24", "26", "28", "30"), + Pair.create("00", "20"), + Pair.create("31", "50")); + assertAllContained(localStrs(cluster.get(3)), + newArrayList("00", "02", "04", "06", "08", "10", + "32", "34", "36", "38", "40", "42", "44", "46", "48"), + Pair.create("11", "30")); + + } + } + + public static void assertAllContained(List current, List expectedTransientKeys, Pair ... ranges) + { + Set cur = Sets.newHashSet(current); + Set expectTransient = Sets.newHashSet(expectedTransientKeys); + for (int i = 0; i < 50; i++) + { + String key = toStr(i); + if (contained(key, ranges)) + assertTrue("NOT IN CURRENT: " + key + " -- " + Arrays.toString(ranges), cur.remove(key)); + else if (expectTransient.remove(key)) + cur.remove(key); + else + assertFalse("SHOULD NOT BE ON NODE: " + key + " -- " + Arrays.toString(ranges) + ": " + current, cur.contains(key)); + } + assertTrue(cur.toString(), cur.isEmpty()); + assertTrue(expectTransient.toString(), expectTransient.isEmpty()); + } + + public static List localStrs(IInvokableInstance inst) + { + List l = new ArrayList<>(); + for (int i = 0; i < 50; i++) + { + Object[][] res = inst.executeInternal("select * from tr.x where id = ?", toStr(i)); + for (Object[] row : res) + l.add((String) row[0]); + } + return l; + } + + private static String toStr(int x) + { + return (x < 10 ? "0" : "") + x; + } + + // ranges [a, b] (not (a, b]) for simplicity + private static boolean contained(String key, Pair ... ranges) + { + for (Pair range : ranges) + if (range.left.compareTo(key) <= 0 && range.right.compareTo(key) >= 0) + return true; + return false; + } + + public static class OPPTokens implements TokenSupplier + { + @Override + public Collection tokens(int i) + { + return Collections.singleton(String.valueOf(i*10)); + } + } + + private static class OPPTokensReplace implements TokenSupplier + { + TokenSupplier base = new OPPTokens(); + @Override + public Collection tokens(int i) + { + if (i == 4) + return base.tokens(2); + return base.tokens(i); + } + } + + public static void populate(Cluster cluster) throws ExecutionException, InterruptedException + { + cluster.schemaChange("create keyspace tr with replication = {'class':'NetworkTopologyStrategy', 'dc0':'3/1'}"); + cluster.schemaChange("create table tr.x (id varchar primary key) with read_repair = 'NONE'"); + for (int i = 0; i < 50; i++) + cluster.coordinator(1).execute("insert into tr.x (id) values (?)", ConsistencyLevel.QUORUM, toStr(i)); + cluster.forEach((i) -> i.nodetoolResult("repair", "-pr", "tr").asserts().success()); + cluster.get(1).shutdown().get(); + for (int i = 0; i < 50; i += 2) + cluster.coordinator(2).execute("insert into tr.x (id) values (?)", ConsistencyLevel.QUORUM, toStr(i)); + cluster.get(1).startup(); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/UpdateSystemAuthAfterDCExpansionTest.java b/test/distributed/org/apache/cassandra/distributed/test/UpdateSystemAuthAfterDCExpansionTest.java index 5313ceefbc..1074fe8368 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/UpdateSystemAuthAfterDCExpansionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/UpdateSystemAuthAfterDCExpansionTest.java @@ -19,11 +19,8 @@ package org.apache.cassandra.distributed.test; import java.util.Collections; -import java.util.UUID; import com.google.common.collect.ImmutableList; - -import org.apache.cassandra.utils.concurrent.Condition; import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; @@ -41,6 +38,11 @@ import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.SingleNodeSequences; +import org.apache.cassandra.tcm.transformations.Unregister; +import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.progress.ProgressEventType; import static java.util.concurrent.TimeUnit.MINUTES; @@ -53,7 +55,6 @@ import static org.apache.cassandra.distributed.shared.AssertUtils.row; import static org.apache.cassandra.distributed.shared.NetworkTopology.dcAndRack; import static org.apache.cassandra.distributed.shared.NetworkTopology.networkTopology; import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; /* @@ -81,13 +82,6 @@ public class UpdateSystemAuthAfterDCExpansionTest extends TestBaseImpl row(username)); } - static void assertRoleAbsent(IInstance instance) - { - assertRows(instance.executeInternal(String.format("SELECT role FROM %s.%s WHERE role = ?", - SchemaConstants.AUTH_KEYSPACE_NAME, ROLES), - username)); - } - static void assertQueryThrowsConfigurationException(Cluster cluster, String query) { cluster.forEach(instance -> { @@ -99,7 +93,11 @@ public class UpdateSystemAuthAfterDCExpansionTest extends TestBaseImpl } catch (Throwable tr) { - assertEquals("org.apache.cassandra.exceptions.ConfigurationException", tr.getClass().getCanonicalName()); + if (tr.getClass().getCanonicalName().equals("java.lang.AssertionError") || + tr.getClass().getCanonicalName().equals("org.apache.cassandra.exceptions.ConfigurationException")) + return; + + throw tr; } }); } @@ -157,9 +155,7 @@ public class UpdateSystemAuthAfterDCExpansionTest extends TestBaseImpl config.set("auto_bootstrap", true); cluster.bootstrap(config).startup(); - // Check that the role is on node1 but has not made it to node2 assertRolePresent(cluster.get(1)); - assertRoleAbsent(cluster.get(2)); // Update options to make sure a replica is in the remote DC logger.debug("Altering '{}' keyspace to use NTS with dc1 & dc2", SchemaConstants.AUTH_KEYSPACE_NAME); @@ -197,15 +193,16 @@ public class UpdateSystemAuthAfterDCExpansionTest extends TestBaseImpl // Forcibly shutdown and have node2 evicted by FD logger.debug("Force shutdown node2"); - String node2hostId = cluster.get(2).callOnInstance(() -> StorageService.instance.getLocalHostId()); + int node2hostId = cluster.get(2).callOnInstance(() -> ClusterMetadata.current().myNodeId().id()); cluster.get(2).shutdown(false); logger.debug("removeNode node2"); cluster.get(1).runOnInstance(() -> { - UUID hostId = UUID.fromString(node2hostId); - InetAddressAndPort endpoint = StorageService.instance.getEndpointForHostId(hostId); + NodeId nodeId = new NodeId(node2hostId); + InetAddressAndPort endpoint = ClusterMetadata.current().directory.endpoint(nodeId); FailureDetector.instance.forceConviction(endpoint); - StorageService.instance.removeNode(node2hostId); + SingleNodeSequences.removeNode(nodeId, true); + Unregister.unregister(nodeId); }); logger.debug("Remove replication to decomissioned dc2"); diff --git a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailDiskUsageTest.java b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailDiskUsageTest.java index b2bb8ea098..91fb396918 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailDiskUsageTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailDiskUsageTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.AuthenticationException; import com.datastax.driver.core.exceptions.InvalidQueryException; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; @@ -63,7 +64,7 @@ public class GuardrailDiskUsageTest extends GuardrailTester private static Session driverSession; @BeforeClass - public static void setupCluster() throws IOException + public static void setupCluster() throws IOException, InterruptedException { // speed up the task that calculates and propagates the disk usage info CassandraRelevantProperties.DISK_USAGE_MONITOR_INTERVAL_MS.setInt(100); @@ -76,17 +77,24 @@ public class GuardrailDiskUsageTest extends GuardrailTester .set("data_disk_usage_percentage_fail_threshold", 99) .set("authenticator", "PasswordAuthenticator")) .start(), 1); - Auth.waitForExistingRoles(cluster.get(1)); - - // create a regular user, since the default superuser is excluded from guardrails - com.datastax.driver.core.Cluster.Builder builder = com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1"); - try (com.datastax.driver.core.Cluster c = builder.withCredentials("cassandra", "cassandra").build(); - Session session = c.connect()) + com.datastax.driver.core.Cluster.Builder builder = com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1") + .withCredentials("cassandra", "cassandra"); + while (true) { - session.execute("CREATE USER test WITH PASSWORD 'test'"); + // create a regular user, since the default superuser is excluded from guardrails + try (com.datastax.driver.core.Cluster c = builder.build(); + Session session = c.connect()) + { + session.execute("CREATE USER test WITH PASSWORD 'test'"); + break; + } + catch (AuthenticationException e) + { + Thread.sleep(1000L); + // ignore + } } - // connect using that superuser, we use the driver to get access to the client warnings driverCluster = builder.withCredentials("test", "test").build(); driverSession = driverCluster.connect(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailItemsPerCollectionOnSSTableWriteTest.java b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailItemsPerCollectionOnSSTableWriteTest.java index 260752e9d9..615a463d91 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailItemsPerCollectionOnSSTableWriteTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailItemsPerCollectionOnSSTableWriteTest.java @@ -52,7 +52,6 @@ public class GuardrailItemsPerCollectionOnSSTableWriteTest extends GuardrailTest .withConfig(c -> c.set("items_per_collection_warn_threshold", WARN_THRESHOLD) .set("items_per_collection_fail_threshold", FAIL_THRESHOLD)) .start()); - cluster.disableAutoCompaction(KEYSPACE); coordinator = cluster.coordinator(1); } @@ -336,4 +335,12 @@ public class GuardrailItemsPerCollectionOnSSTableWriteTest extends GuardrailTest "this exceeds the failure threshold of %d.", key, qualifiedTableName, numItems, FAIL_THRESHOLD); } + + @Override + protected void schemaChange(String query) + { + super.schemaChange(query); + // Make sure to disable auto compaction for each newly created cf + cluster.disableAutoCompaction(KEYSPACE); + } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/AssassinatedEmptyNodeTest.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/AssassinatedEmptyNodeTest.java index 31a732fb3d..03a92d6b97 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/AssassinatedEmptyNodeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/AssassinatedEmptyNodeTest.java @@ -34,13 +34,6 @@ import static org.apache.cassandra.distributed.shared.ClusterUtils.stopAll; */ public class AssassinatedEmptyNodeTest extends BaseAssassinatedCase { - // empty state does not include the token metadata, so when assassinate happens it will fail to find the token - @Override - protected String expectedMessage(IInvokableInstance nodeToRemove) - { - return "Could not find tokens for " + nodeToRemove.config().broadcastAddress() + " to replace"; - } - @Override void consume(Cluster cluster, IInvokableInstance nodeToRemove) { @@ -56,7 +49,7 @@ public class AssassinatedEmptyNodeTest extends BaseAssassinatedCase peer.startup(); // at this point node2 should be known in gossip, but with generation/version of 0 - assertGossipInfo(seed, addressToReplace, 0, -1); - assertGossipInfo(peer, addressToReplace, 0, -1); + assertGossipInfo(seed, addressToReplace, 0, 0); + assertGossipInfo(peer, addressToReplace, 0, 0); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/BaseAssassinatedCase.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/BaseAssassinatedCase.java index 353732cc55..d1bd5ce417 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/BaseAssassinatedCase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/BaseAssassinatedCase.java @@ -33,7 +33,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_ import static org.apache.cassandra.distributed.shared.ClusterUtils.assertRingState; import static org.apache.cassandra.distributed.shared.ClusterUtils.awaitGossipStatus; import static org.apache.cassandra.distributed.shared.ClusterUtils.getBroadcastAddressHostWithPortString; -import static org.apache.cassandra.distributed.shared.ClusterUtils.getTokens; import static org.apache.cassandra.distributed.shared.ClusterUtils.replaceHostAndStart; import static org.apache.cassandra.distributed.test.hostreplacement.HostReplacementTest.setupCluster; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -50,11 +49,6 @@ public abstract class BaseAssassinatedCase extends TestBaseImpl { } - protected String expectedMessage(IInvokableInstance nodeToRemove) - { - return "Cannot replace token " + getTokens(nodeToRemove).get(0) + " which does not exist!"; - } - @Test public void test() throws IOException { @@ -94,7 +88,7 @@ public abstract class BaseAssassinatedCase extends TestBaseImpl // matter for this test properties.set(BOOTSTRAP_SCHEMA_DELAY_MS, 10); })) - .hasMessage(expectedMessage(nodeToRemove)); + .hasMessageContaining("Cannot replace node /127.0.0.2:7012 which is not currently joined"); } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/FailedBootstrapTest.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/FailedBootstrapTest.java index 7a8e42186a..5c906a1449 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/FailedBootstrapTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/FailedBootstrapTest.java @@ -23,9 +23,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.TimeoutException; import org.junit.Test; import org.slf4j.Logger; @@ -34,26 +32,22 @@ import org.slf4j.LoggerFactory; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.implementation.bind.annotation.SuperCall; import net.bytebuddy.implementation.bind.annotation.This; -import org.apache.cassandra.auth.CassandraRoleManager; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.metrics.ClientRequestsMetricsHolder; import org.apache.cassandra.streaming.StreamException; import org.apache.cassandra.streaming.StreamResultFuture; -import org.assertj.core.api.Assertions; -import org.awaitility.Awaitility; import static net.bytebuddy.matcher.ElementMatchers.named; -import static org.apache.cassandra.config.CassandraRelevantProperties.SUPERUSER_SETUP_DELAY_MS; -import static org.apache.cassandra.distributed.shared.ClusterUtils.replaceHostAndStart; import static org.apache.cassandra.distributed.shared.ClusterUtils.stopUnchecked; import static org.apache.cassandra.distributed.test.hostreplacement.HostReplacementTest.setupCluster; +import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance; +import static org.apache.cassandra.distributed.shared.ClusterUtils.startHostReplacement; public class FailedBootstrapTest extends TestBaseImpl { @@ -62,7 +56,7 @@ public class FailedBootstrapTest extends TestBaseImpl private static final int NODE_TO_REMOVE = 2; @Test - public void roleSetupDoesNotProduceUnavailables() throws IOException + public void test() throws IOException, InterruptedException, TimeoutException { Cluster.Builder builder = Cluster.build(3) .withConfig(c -> c.with(Feature.values())) @@ -79,26 +73,15 @@ public class FailedBootstrapTest extends TestBaseImpl stopUnchecked(nodeToRemove); // should fail to join, but should start up! - IInvokableInstance added = replaceHostAndStart(cluster, nodeToRemove, p -> p.set(SUPERUSER_SETUP_DELAY_MS, "1")); - // log gossip for debugging + IInstanceConfig toReplaceConf = nodeToRemove.config(); + IInvokableInstance added = addInstance(cluster, toReplaceConf, c -> c.set("auto_bootstrap", true)); + startHostReplacement(nodeToRemove, added, (a_, b_) -> {}); + added.logs().watchFor("Node is not yet bootstrapped completely"); alive.forEach(i -> { NodeToolResult result = i.nodetoolResult("gossipinfo"); result.asserts().success(); logger.info("gossipinfo for node{}\n{}", i.config().num(), result.getStdout()); }); - - // CassandraRoleManager attempted to do distributed reads while bootstrap was still going (it failed, so still in bootstrap mode) - // so need to validate that is no longer happening and we incrementing org.apache.cassandra.metrics.ClientRequestMetrics.unavailables - // sleep larger than multiple retry attempts... - Awaitility.await() - .atMost(1, TimeUnit.MINUTES) - .until(() -> added.callOnInstance(() -> BB.SETUP_SCHEDULE_COUNTER.get()) >= 42); // why 42? just need something large enough to make sure multiple attempts happened - - // do we have any read metrics have unavailables? - added.runOnInstance(() -> { - Assertions.assertThat(ClientRequestsMetricsHolder.readMetrics.unavailables.getCount()).describedAs("read unavailables").isEqualTo(0); - Assertions.assertThat(ClientRequestsMetricsHolder.casReadMetrics.unavailables.getCount()).describedAs("CAS read unavailables").isEqualTo(0); - }); } } @@ -114,12 +97,6 @@ public class FailedBootstrapTest extends TestBaseImpl .intercept(MethodDelegation.to(BB.class)) .make() .load(classLoader, ClassLoadingStrategy.Default.INJECTION); - - new ByteBuddy().rebase(CassandraRoleManager.class) - .method(named("scheduleSetupTask")) - .intercept(MethodDelegation.to(BB.class)) - .make() - .load(classLoader, ClassLoadingStrategy.Default.INJECTION); } public static void maybeComplete(@This StreamResultFuture future) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException @@ -128,12 +105,5 @@ public class FailedBootstrapTest extends TestBaseImpl method.setAccessible(true); method.invoke(future, new StreamException(future.getCurrentState(), "Stream failed")); } - - private static final AtomicInteger SETUP_SCHEDULE_COUNTER = new AtomicInteger(0); - public static void scheduleSetupTask(final Callable setupTask, @SuperCall Runnable fn) - { - SETUP_SCHEDULE_COUNTER.incrementAndGet(); - fn.run(); - } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementOfDownedClusterTest.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementOfDownedClusterTest.java index 477e226d0b..5ed24ad400 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementOfDownedClusterTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementOfDownedClusterTest.java @@ -19,13 +19,17 @@ package org.apache.cassandra.distributed.test.hostreplacement; import java.io.IOException; -import java.net.InetSocketAddress; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.monitoring.runtime.instrumentation.common.util.concurrent.Uninterruptibles; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.Feature; @@ -33,16 +37,21 @@ import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; import org.assertj.core.api.Assertions; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIPER_QUARANTINE_DELAY; -import static org.apache.cassandra.distributed.shared.ClusterUtils.assertGossipInfo; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance; import static org.apache.cassandra.distributed.shared.ClusterUtils.assertNotInRing; import static org.apache.cassandra.distributed.shared.ClusterUtils.assertRingIs; import static org.apache.cassandra.distributed.shared.ClusterUtils.awaitRingHealthy; import static org.apache.cassandra.distributed.shared.ClusterUtils.awaitRingJoin; import static org.apache.cassandra.distributed.shared.ClusterUtils.getTokenMetadataTokens; import static org.apache.cassandra.distributed.shared.ClusterUtils.replaceHostAndStart; +import static org.apache.cassandra.distributed.shared.ClusterUtils.startHostReplacement; import static org.apache.cassandra.distributed.shared.ClusterUtils.stopAll; import static org.apache.cassandra.distributed.test.hostreplacement.HostReplacementTest.setupCluster; import static org.apache.cassandra.distributed.test.hostreplacement.HostReplacementTest.validateRows; @@ -70,13 +79,14 @@ public class HostReplacementOfDownedClusterTest extends TestBaseImpl // start with 2 nodes, stop both nodes, start the seed, host replace the down node) TokenSupplier even = TokenSupplier.evenlyDistributedTokens(2); try (Cluster cluster = Cluster.build(2) - .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK)) + .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK) + .set("progress_barrier_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms")) .withTokenSupplier(node -> even.token(node == 3 ? 2 : node)) .start()) { IInvokableInstance seed = cluster.get(1); IInvokableInstance nodeToRemove = cluster.get(2); - InetSocketAddress addressToReplace = nodeToRemove.broadcastAddress(); setupCluster(cluster); @@ -90,9 +100,6 @@ public class HostReplacementOfDownedClusterTest extends TestBaseImpl // with all nodes down, now start the seed (should be first node) seed.startup(); - // at this point node2 should be known in gossip, but with generation/version of 0 - assertGossipInfo(seed, addressToReplace, 0, -1); - // make sure node1 still has node2's tokens List currentTokens = getTokenMetadataTokens(seed); Assertions.assertThat(currentTokens) @@ -100,7 +107,10 @@ public class HostReplacementOfDownedClusterTest extends TestBaseImpl .isEqualTo(beforeCrashTokens); // now create a new node to replace the other node - IInvokableInstance replacingNode = replaceHostAndStart(cluster, nodeToRemove); + IInvokableInstance replacingNode = addInstance(cluster, nodeToRemove.config(), + c -> c.set("auto_bootstrap", true) + .set("progress_barrier_min_consistency_level", ConsistencyLevel.ONE)); + startHostReplacement(nodeToRemove, replacingNode, (ignore1_, ignore2_) -> {}); awaitRingJoin(seed, replacingNode); awaitRingJoin(replacingNode, seed); @@ -123,14 +133,16 @@ public class HostReplacementOfDownedClusterTest extends TestBaseImpl int numStartNodes = 3; TokenSupplier even = TokenSupplier.evenlyDistributedTokens(numStartNodes); try (Cluster cluster = Cluster.build(numStartNodes) - .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK)) + .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK) + .set("progress_barrier_min_consistency_level", ConsistencyLevel.ONE) + .set("progress_barrier_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms")) .withTokenSupplier(node -> even.token(node == (numStartNodes + 1) ? 2 : node)) .start()) { IInvokableInstance seed = cluster.get(1); IInvokableInstance nodeToRemove = cluster.get(2); IInvokableInstance nodeToStartAfterReplace = cluster.get(3); - InetSocketAddress addressToReplace = nodeToRemove.broadcastAddress(); setupCluster(cluster); @@ -140,19 +152,36 @@ public class HostReplacementOfDownedClusterTest extends TestBaseImpl // now stop all nodes stopAll(cluster); - // with all nodes down, now start the seed (should be first node) seed.startup(); - // at this point node2 should be known in gossip, but with generation/version of 0 - assertGossipInfo(seed, addressToReplace, 0, -1); - // make sure node1 still has node2's tokens List currentTokens = getTokenMetadataTokens(seed); Assertions.assertThat(currentTokens) .as("Tokens no longer match after restarting") .isEqualTo(beforeCrashTokens); + cluster.get(1).runOnInstance(() -> { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60); + while (System.nanoTime() < deadline) + { + int down = 0; + Set downNodes = new HashSet<>(); + for (Map.Entry e : Gossiper.instance.endpointStateMap.entrySet()) + { + if (!e.getValue().isAlive()) + downNodes.add(e.getKey()); + } + if (downNodes.size() >= 2) + { + logger.info("Found down nodes: " + downNodes); + return; + } + logger.warn(String.format("Only %d down. Sleeping.", down)); + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + throw new RuntimeException("Nodes did not appear as down."); + }); // now create a new node to replace the other node IInvokableInstance replacingNode = replaceHostAndStart(cluster, nodeToRemove); diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java index 8219d43ad1..363b1bf4d9 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java @@ -34,13 +34,13 @@ import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.shared.AssertUtils; -import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.schema.SchemaConstants; import org.assertj.core.api.Assertions; import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIPER_QUARANTINE_DELAY; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.ClusterUtils.assertInRing; import static org.apache.cassandra.distributed.shared.ClusterUtils.assertRingIs; import static org.apache.cassandra.distributed.shared.ClusterUtils.awaitRingHealthy; @@ -81,7 +81,7 @@ public class HostReplacementTest extends TestBaseImpl setupCluster(cluster); // collect rows to detect issues later on if the state doesn't match - SimpleQueryResult expectedState = nodeToRemove.coordinator().executeWithResult("SELECT * FROM " + KEYSPACE + ".tbl", ConsistencyLevel.ALL); + SimpleQueryResult expectedState = seed.coordinator().executeWithResult("SELECT * FROM " + KEYSPACE + ".tbl", ConsistencyLevel.ALL); stopUnchecked(nodeToRemove); @@ -102,6 +102,8 @@ public class HostReplacementTest extends TestBaseImpl assertRingIs(seed, seed, replacingNode); logger.info("Current ring is {}", assertRingIs(replacingNode, seed, replacingNode)); + assertRows(replacingNode.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl"), + expectedState.toObjectArrays()); validateRows(seed.coordinator(), expectedState); validateRows(replacingNode.coordinator(), expectedState); } @@ -111,6 +113,7 @@ public class HostReplacementTest extends TestBaseImpl * Attempt to do a host replacement on a alive host */ @Test + // TODO this might actually be safe now (though probably still undesirable), public void replaceAliveHost() throws IOException { // start with 2 nodes, stop both nodes, start the seed, host replace the down node) @@ -170,6 +173,11 @@ public class HostReplacementTest extends TestBaseImpl SimpleQueryResult expectedState = nodeToRemove.coordinator().executeWithResult("SELECT * FROM " + KEYSPACE + ".tbl", ConsistencyLevel.ALL); List beforeCrashTokens = getTokenMetadataTokens(seed); + // TODO the node acting as the CMS must flush its distributed_metadata_log table before shutdown + // this should be a temporary hack + seed.flush("system"); + seed.flush(SchemaConstants.METADATA_KEYSPACE_NAME); + // shutdown the seed, then the node to remove stopUnchecked(seed); stopUnchecked(nodeToRemove); @@ -211,8 +219,6 @@ public class HostReplacementTest extends TestBaseImpl fixDistributedSchemas(cluster); init(cluster); - ClusterUtils.awaitGossipSchemaMatch(cluster); - populate(cluster); cluster.forEach(i -> i.flush(KEYSPACE)); } @@ -232,6 +238,6 @@ public class HostReplacementTest extends TestBaseImpl { expected.reset(); SimpleQueryResult rows = coordinator.executeWithResult("SELECT * FROM " + KEYSPACE + ".tbl", ConsistencyLevel.ALL); - AssertUtils.assertRows(rows, expected); + assertRows(rows, expected); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest.java b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest.java index 7d692487be..88d478fc3c 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest.java @@ -30,7 +30,6 @@ import org.junit.Test; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.implementation.bind.annotation.SuperCall; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.api.Feature; @@ -40,13 +39,15 @@ import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.impl.InstanceIDDefiner; import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.service.PendingRangeCalculatorService; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.Shared; import org.assertj.core.api.Assertions; import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.Constants.KEY_DTEST_FULL_STARTUP; +// This test requires us to allow replace with the same address. public class NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest extends TestBaseImpl { @Test @@ -79,7 +80,8 @@ public class NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest extends Te SharedState.shutdownComplete.await(1, TimeUnit.MINUTES); } - IInvokableInstance inst = ClusterUtils.addInstance(cluster, toReplace.config(), c -> c.set("auto_bootstrap", true)); + IInvokableInstance inst = ClusterUtils.addInstance(cluster, toReplace.config(), c -> c.set(KEY_DTEST_FULL_STARTUP, false) + .set("auto_bootstrap", true)); ClusterUtils.updateAddress(inst, toReplaceAddress); Assertions.assertThatThrownBy(() -> inst.startup()) .hasMessageContaining("A node with address") @@ -98,8 +100,8 @@ public class NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest extends Te private static void shutdownBeforeNormal(ClassLoader cl) { - new ByteBuddy().rebase(PendingRangeCalculatorService.class) - .method(named("blockUntilFinished")) + new ByteBuddy().rebase(StorageService.class) + .method(named("doAuthSetup")) .intercept(MethodDelegation.to(ShutdownBeforeNormal.class)) .make() .load(cl, ClassLoadingStrategy.Default.INJECTION); @@ -117,9 +119,8 @@ public class NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest extends Te public static class ShutdownBeforeNormal { - public static void blockUntilFinished(@SuperCall Runnable fn) + public static void doAuthSetup() { - fn.run(); int id = Integer.parseInt(InstanceIDDefiner.getInstanceId().replace("node", "")); ICluster cluster = Objects.requireNonNull(SharedState.cluster); // can't stop here as the stop method and start method share a lock; and block gets called in start... @@ -128,6 +129,7 @@ public class NodeCannotJoinAsHibernatingNodeWithoutReplaceAddressTest extends Te SharedState.shutdownComplete.countDown(); }); JVMStabilityInspector.killCurrentJVM(new RuntimeException("Attempting to stop the instance"), false); + throw new RuntimeException(); } } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java index 5f21c94ab3..fa379ffdc1 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java @@ -44,8 +44,9 @@ public class JMXGetterCheckTest extends TestBaseImpl { private static final Set IGNORE_ATTRIBUTES = ImmutableSet.of( "org.apache.cassandra.net:type=MessagingService:BackPressurePerHost", // throws unsupported saying the feature was removed... dropped in CASSANDRA-15375 - "org.apache.cassandra.db:type=DynamicEndpointSnitch:Scores" // when running in multiple-port-one-IP mode, this fails - + "org.apache.cassandra.db:type=DynamicEndpointSnitch:Scores", // when running in multiple-port-one-IP mode, this fails + "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersions", // deprecated (TCM) + "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersionsWithPort" // deprecated (TCM) ); private static final Set IGNORE_OPERATIONS = ImmutableSet.of( "org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit @@ -57,7 +58,9 @@ public class JMXGetterCheckTest extends TestBaseImpl "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // CIDR cache isn't enabled by default "org.apache.cassandra.db:type=StorageService:clearConnectionHistory", // Throws a NullPointerException "org.apache.cassandra.db:type=StorageService:startGossiping", // causes multiple loops to fail - "org.apache.cassandra.db:type=StorageService:startNativeTransport" // causes multiple loops to fail + "org.apache.cassandra.db:type=StorageService:startNativeTransport", // causes multiple loops to fail + "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // AllowAllCIDRAuthorizer doesn't support this operation, as feature is disabled by default + "org.apache.cassandra.db:type=StorageService:forceRemoveCompletion" // deprecated (TCM) ); @Test diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BootWithMetadataTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BootWithMetadataTest.java new file mode 100644 index 0000000000..fd554a189a --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BootWithMetadataTest.java @@ -0,0 +1,140 @@ +/* + * 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.log; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutionException; + +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.CMSOperations; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.start; +import static org.junit.Assert.assertEquals; +import static org.psjava.util.AssertStatus.assertTrue; + +public class BootWithMetadataTest extends TestBaseImpl +{ + @Test + public void resetTest() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(builder().withNodes(3) + .start())) + { + long epoch = 0; + for (int i = 0; i < 10; i++) + { + cluster.schemaChange(withKeyspace("create table %s.x" + i + " (id int primary key)")); + // later we reset to `epoch` - only tables x0 .. x5 should exist + if (i == 5) + epoch = cluster.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + } + + long resetEpoch = epoch; + String filename = cluster.get(1).callOnInstance(() -> { + try + { + return CMSOperations.instance.dumpClusterMetadata(resetEpoch, 1000, "V2"); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + cluster.get(1).shutdown().get(); + + start(cluster.get(1), (props) -> props.set(CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA, filename)); + + cluster.schemaChange(withKeyspace("create table %s.yy (id int primary key)")); + cluster.forEach(() -> { + assertEquals(1, ClusterMetadata.current().fullCMSMembers().size()); + assertTrue(ClusterMetadata.current().fullCMSMembers().contains(InetAddressAndPort.getByNameUnchecked("127.0.0.1"))); + Keyspace ks = Keyspace.open(KEYSPACE); + assertEquals(6, ks.getColumnFamilyStores().size()); + for (int i = 0; i < 6; i++) + assertTrue(ks.getColumnFamilyStore("x"+i) != null); // getColumnFamilyStore throws + }); + } + } + + @Test + public void newCMSTest() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(builder().withNodes(4) + .start())) + { + for (int i = 0; i < 10; i++) + cluster.schemaChange(withKeyspace("create table %s.x" + i + " (id int primary key)")); + + String filename = cluster.get(1).callOnInstance(() -> { + try + { + ClusterMetadata metadata = ClusterMetadata.current(); + Replica oldCMS = EntireRange.replica(InetAddressAndPort.getByNameUnchecked("127.0.0.1")); + Replica newCMS = EntireRange.replica(InetAddressAndPort.getByNameUnchecked("127.0.0.2")); + ClusterMetadata.Transformer transformer = metadata.transformer(); + DataPlacement.Builder builder = metadata.placements.get(ReplicationParams.meta(metadata)).unbuild() + .withoutReadReplica(metadata.nextEpoch(), oldCMS) + .withoutWriteReplica(metadata.nextEpoch(), oldCMS) + .withWriteReplica(metadata.nextEpoch(), newCMS) + .withReadReplica(metadata.nextEpoch(), newCMS); + transformer = transformer.with(metadata.placements.unbuild().with(ReplicationParams.meta(metadata), builder.build()).build()); + ClusterMetadata toDump = transformer.build().metadata.forceEpoch(Epoch.create(1000)); + Path p = Files.createTempFile("clustermetadata", "dump"); + try (FileOutputStreamPlus out = new FileOutputStreamPlus(p)) + { + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, toDump, out, NodeVersion.CURRENT_METADATA_VERSION); + } + return p.toString(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + cluster.get(1).shutdown().get(); + cluster.get(2).shutdown().get(); + start(cluster.get(2), (props) -> props.set(CassandraRelevantProperties.TCM_UNSAFE_BOOT_WITH_CLUSTERMETADATA, filename)); + for (int i = 2; i <= 4; i++) + cluster.get(i).runOnInstance(() -> { + assertEquals(1, ClusterMetadata.current().fullCMSMembers().size()); + assertTrue(ClusterMetadata.current().fullCMSMembers().contains(InetAddressAndPort.getByNameUnchecked("127.0.0.2"))); + }); + + cluster.coordinator(3).execute(withKeyspace("create table %s.yy (id int primary key)"), ConsistencyLevel.ONE); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java new file mode 100644 index 0000000000..f75269c540 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test.log; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Test; + +import harry.core.Configuration; +import harry.ddl.SchemaSpec; +import harry.visitors.MutatingVisitor; +import harry.visitors.QueryLogger; +import harry.visitors.RandomPartitionValidator; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.harry.ClusterState; +import org.apache.cassandra.distributed.harry.ExistingClusterSUT; +import org.apache.cassandra.distributed.harry.FlaggedRunner; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static harry.core.Configuration.VisitorPoolConfiguration.pool; +import static harry.ddl.ColumnSpec.asciiType; +import static harry.ddl.ColumnSpec.ck; +import static harry.ddl.ColumnSpec.int64Type; +import static harry.ddl.ColumnSpec.pk; +import static harry.ddl.ColumnSpec.regularColumn; +import static harry.ddl.ColumnSpec.staticColumn; +import static java.util.Arrays.asList; +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; +import static org.junit.Assert.fail; +import static org.psjava.util.AssertStatus.assertTrue; + +public class BounceGossipTest extends TestBaseImpl +{ + @Test + public void bounceTest() throws Exception + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .start())) + { + ExecutorService es = executorFactory().pooled("harry", 1); + SchemaSpec schema = new SchemaSpec("harry", "test_table", + asList(pk("pk1", asciiType), pk("pk1", int64Type)), + asList(ck("ck1", asciiType), ck("ck1", int64Type)), + asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)), + asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type))); + AtomicInteger down = new AtomicInteger(0); + ClusterState clusterState = (i) -> down.get() == i; + Configuration config = Configuration.fromYamlString(createHarryConf()) + .unbuild() + .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3)) + .setSUT(new ExistingClusterSUT(cluster, clusterState)) + .build(); + + CountDownLatch stopLatch = CountDownLatch.newCountDownLatch(1); + Future f = es.submit(() -> { + try + { + new FlaggedRunner(config.createRun(), + config, + asList(pool("Writer", 1, MutatingVisitor::new), + pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))), + stopLatch).run(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); + + down.set(3); + cluster.get(3).shutdown(true).get(); + cluster.get(1).logs().watchFor("/127.0.0.3:.* is now DOWN"); + Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); + cluster.get(3).startup(); + cluster.get(1).logs().watchFor("/127.0.0.3:.* is now UP"); + down.set(0); + stopLatch.decrement(); + f.get(); + + for (int inst = 1; inst <= 3; inst++) + { + cluster.get(inst).runOnInstance(() -> { + for (int i = 1; i <= 3; i++) + { + boolean stateOk = false; + int tries = 0; + while (!stateOk) + { + EndpointState epstate = Gossiper.instance.getEndpointStateForEndpoint(InetAddressAndPort.getByNameUnchecked("127.0.0." + i)); + stateOk = epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL") && + epstate.getApplicationState(ApplicationState.TOKENS) != null && + epstate.getHeartBeatState().getGeneration() > 0; + if (!stateOk) + { + tries++; + if (tries > 20) + { + assertTrue(epstate.getApplicationState(ApplicationState.STATUS_WITH_PORT).value.contains("NORMAL")); + assertTrue(epstate.getApplicationState(ApplicationState.TOKENS) != null); + assertTrue(epstate.getHeartBeatState().getGeneration() > 0); + fail("shouldn't reach this, but epstate: "+epstate); + } + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + } + } + }); + + } + + } + } + + @Test + public void gossipPropagatesVersionTest() throws IOException + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .withoutVNodes() + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + int tokensBefore = getGossipTokensVersion(cluster, 2); + cluster.get(2).nodetoolResult("move", "9999").asserts().success(); + int correctTokensVersion; + while ((correctTokensVersion = getGossipTokensVersion(cluster, 2)) == tokensBefore) + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); // wait for LegacyStateListener to actually update gossip + for (int inst : new int[] {1, 3}) + while (correctTokensVersion != getGossipTokensVersion(cluster, inst)) + { + System.out.println(correctTokensVersion + " ::: " + getGossipTokensVersion(cluster, inst)); + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); // wait for gossip to propagate + correctTokensVersion = getGossipTokensVersion(cluster, 2); + } + } + } + + private static int getGossipTokensVersion(Cluster cluster, int instance) + { + return cluster.get(instance).callOnInstance(() -> Gossiper.instance.endpointStateMap.get(InetAddressAndPort.getByNameUnchecked("127.0.0.2")) + .getApplicationState(ApplicationState.TOKENS).version); + } + + private static String createHarryConf() + { + return "seed: " + currentTimeMillis() + "\n" + + "\n" + + "# Default schema provider generates random schema\n" + + "schema_provider:\n" + + " fixed:\n" + + " keyspace: harry\n" + + " table: test_table\n" + + " partition_keys:\n" + + " pk1: bigint\n" + + " pk2: ascii\n" + + " clustering_keys:\n" + + " ck1: ascii\n" + + " ck2: bigint\n" + + " regular_columns:\n" + + " v1: ascii\n" + + " v2: bigint\n" + + " v3: ascii\n" + + " v4: bigint\n" + + " static_keys:\n" + + " s1: ascii\n" + + " s2: bigint\n" + + " s3: ascii\n" + + " s4: bigint\n" + + "\n" + + "clock:\n" + + " offset:\n" + + " offset: 1000\n" + + "\n" + + "drop_schema: false\n" + + "create_schema: true\n" + + "truncate_table: true\n" + + "\n" + + "partition_descriptor_selector:\n" + + " default:\n" + + " window_size: 10\n" + + " slide_after_repeats: 100\n" + + "\n" + + "clustering_descriptor_selector:\n" + + " default:\n" + + " modifications_per_lts:\n" + + " type: \"constant\"\n" + + " constant: 2\n" + + " rows_per_modification:\n" + + " type: \"constant\"\n" + + " constant: 2\n" + + " operation_kind_weights:\n" + + " DELETE_RANGE: 0\n" + + " DELETE_SLICE: 0\n" + + " DELETE_ROW: 0\n" + + " DELETE_COLUMN: 0\n" + + " DELETE_PARTITION: 0\n" + + " DELETE_COLUMN_WITH_STATICS: 0\n" + + " INSERT_WITH_STATICS: 50\n" + + " INSERT: 50\n" + + " UPDATE_WITH_STATICS: 50\n" + + " UPDATE: 50\n" + + " column_mask_bitsets: null\n" + + " max_partition_size: 1000\n" + + "\n" + + "metric_reporter:\n" + + " no_op: {}\n" + + "\n" + + "data_tracker:\n" + + " locking:\n" + + " max_seen_lts: -1\n" + + " max_complete_lts: -1"; + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BounceIndexRebuildTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BounceIndexRebuildTest.java new file mode 100644 index 0000000000..3fb2178e12 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceIndexRebuildTest.java @@ -0,0 +1,52 @@ +/* + * 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.log; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; + +import static org.junit.Assert.assertEquals; + +public class BounceIndexRebuildTest extends TestBaseImpl +{ + @Test + public void bounceTest() throws Exception + { + try (Cluster cluster = init(builder().withNodes(1) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key, x int)")); + for (int i = 0; i < 10; i++) + cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (id, x) values (?, ?)"), ConsistencyLevel.ALL, i, i); + + cluster.schemaChange(withKeyspace("create index idx on %s.tbl (x)")); + Object[][] res = cluster.coordinator(1).execute(withKeyspace("select * from %s.tbl where x=5"), ConsistencyLevel.ALL); + assert res.length > 0; + + cluster.get(1).shutdown().get(); + cluster.get(1).startup(); + assertEquals(1, cluster.get(1).logs().grep("Index build of idx complete").getResult().size()); + res = cluster.coordinator(1).execute(withKeyspace("select * from %s.tbl where x=5"), ConsistencyLevel.ALL); + assert res.length > 0; + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BounceResetHostIdTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BounceResetHostIdTest.java new file mode 100644 index 0000000000..0411c41617 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceResetHostIdTest.java @@ -0,0 +1,52 @@ +/* + * 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.log; + +import java.util.UUID; + +import org.junit.Test; + +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.membership.NodeId; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BounceResetHostIdTest extends TestBaseImpl +{ + @Test + public void bounceTest() throws Exception + { + try (Cluster cluster = init(builder().withNodes(1) + .start())) + { + String wrongId = UUID.randomUUID().toString(); + cluster.get(1).runOnInstance(() -> { + SystemKeyspace.setLocalHostId(UUID.fromString(wrongId)); + assertFalse(NodeId.isValidNodeId(SystemKeyspace.getLocalHostId())); + }); + cluster.get(1).shutdown().get(); + cluster.get(1).startup(); + cluster.get(1).logs().watchFor("NodeId is wrong, updating from "+wrongId+" to "+(new NodeId(1).toUUID())); + cluster.get(1).runOnInstance(() -> assertTrue(NodeId.isValidNodeId(SystemKeyspace.getLocalHostId()))); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CMSMembershipMetricsTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/CMSMembershipMetricsTest.java new file mode 100644 index 0000000000..a165f39848 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CMSMembershipMetricsTest.java @@ -0,0 +1,119 @@ +/* + * 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.log; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.impl.DistributedTestSnitch; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.awaitility.Awaitility; + +import static org.junit.Assert.assertEquals; + +public class CMSMembershipMetricsTest extends TestBaseImpl +{ + @Test + public void testCMSMembershipMetrics() throws Exception + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(c -> c.with(Feature.NETWORK, Feature.GOSSIP)) // needed for addtocms below + .start())) + { + cluster.forEach(i -> assertMembershipSize(1, i)); + cluster.forEach(i -> assertUnreachableCMSMembers(0, i)); + assertCMSMembership(1, cluster.get(1)); + assertCMSMembership(0, cluster.get(2), cluster.get(3)); + + cluster.get(2).runOnInstance(() -> AddToCMS.initiate()); + cluster.forEach(i -> assertMembershipSize(2, i)); + cluster.forEach(i -> assertUnreachableCMSMembers(0, i)); + assertCMSMembership(1, cluster.get(1), cluster.get(2)); + assertCMSMembership(0, cluster.get(3)); + + cluster.get(3).runOnInstance(() -> AddToCMS.initiate()); + cluster.forEach(i -> assertMembershipSize(3, i)); + cluster.forEach(i -> assertUnreachableCMSMembers(0, i)); + assertCMSMembership(1, cluster.get(1), cluster.get(2), cluster.get(3)); + + // mark node3 down and ensure it stays that way + cluster.filters().allVerbs().from(3).to(1,2).drop(); + cluster.filters().allVerbs().from(1,2).to(3).drop(); + markDown(cluster.get(3), cluster.get(1)); + markDown(cluster.get(3), cluster.get(2)); + + cluster.forEach(i -> assertMembershipSize(3, i)); + assertUnreachableCMSMembers(1, cluster.get(1)); + assertUnreachableCMSMembers(1, cluster.get(2)); + assertUnreachableCMSMembers(0, cluster.get(3)); + + cluster.filters().reset(); + markUp(cluster.get(3), cluster.get(1)); + markUp(cluster.get(3), cluster.get(2)); + cluster.forEach(i -> assertMembershipSize(3, i)); + cluster.forEach(i -> assertUnreachableCMSMembers(0, i)); + } + } + + private void assertMembershipSize(long expected, IInvokableInstance instance) + { + long actual = instance.callOnInstance(() -> TCMMetrics.instance.currentCMSSize.getValue()); + assertEquals(String.format("Expected %s CMS members, node %s disagrees", expected, instance.config().num()), + expected, actual); + } + + private void assertUnreachableCMSMembers(long expected, IInvokableInstance instance) + { + long actual = instance.callOnInstance(() -> TCMMetrics.instance.unreachableCMSMembers.getValue()); + assertEquals(String.format("Expected %s down CMS members, node %s disagrees", expected, instance.config().num()), + expected, actual); + } + + void assertCMSMembership(int expected, IInvokableInstance... instances) + { + for(IInvokableInstance instance: instances) + { + int actual = instance.callOnInstance(() -> TCMMetrics.instance.isCMSMember.getValue()); + assertEquals(String.format("Expected isCMSMember value to be %s, node %s disagrees", expected, instance.config().num()), + expected, actual); + } + } + + private void markDown(IInvokableInstance down, IInvokableInstance inst) + { + InetSocketAddress node3Address = down.config().broadcastAddress(); + inst.runOnInstance(() -> FailureDetector.instance.forceConviction(DistributedTestSnitch.toCassandraInetAddressAndPort(node3Address))); + } + + private void markUp(IInvokableInstance down, IInvokableInstance inst) + { + InetSocketAddress downAddress = down.config().broadcastAddress(); + inst.runOnInstance(() -> FailureDetector.instance.report(DistributedTestSnitch.toCassandraInetAddressAndPort(downAddress))); + Awaitility.waitAtMost(10, TimeUnit.SECONDS) + .until(() -> inst.callOnInstance(() -> FailureDetector.instance.isAlive(DistributedTestSnitch.toCassandraInetAddressAndPort(downAddress)))); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java new file mode 100644 index 0000000000..b262b66b00 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java @@ -0,0 +1,131 @@ +/* + * 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.log; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.locator.IEndpointSnitch; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.SchemaProvider; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Processor; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.transformations.AlterSchema; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.utils.FBUtilities; +import org.mockito.Mockito; + +public class CMSTestBase +{ + static + { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setEndpointSnitch(new IEndpointSnitch() + { + public String getRack(InetAddressAndPort endpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + return metadata.directory.location(metadata.directory.peerId(endpoint)).rack; + } + public String getDatacenter(InetAddressAndPort endpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + return metadata.directory.location(metadata.directory.peerId(endpoint)).datacenter; + } + public > C sortedByProximity(InetAddressAndPort address, C addresses) {return null;} + public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) {return 0;} + public void gossiperStarting() {} + public boolean isWorthMergingForRangeQuery(ReplicaCollection merged, ReplicaCollection l1, ReplicaCollection l2) {return false;} + }); + DatabaseDescriptor.setDefaultKeyspaceRF(1); + Guardrails.instance.setMinimumReplicationFactorThreshold(1, 1); + + try + { + DatabaseDescriptor.setBroadcastAddress(InetAddress.getByName("127.0.0.1")); + } + catch (UnknownHostException e) + { + e.printStackTrace(); + } + } + + public static class CMSSut implements AutoCloseable + { + public final Murmur3Partitioner partitioner; + public final LocalLog log; + public final ClusterMetadataService service; + public final SchemaProvider schemaProvider; + public final PlacementSimulator.ReplicationFactor rf; + + public CMSSut(IIsolatedExecutor.SerializableFunction processorFactory, boolean addListeners, PlacementSimulator.ReplicationFactor rf) + { + partitioner = Murmur3Partitioner.instance; + this.rf = rf; + schemaProvider = Mockito.mock(SchemaProvider.class); + ClusterMetadata initial = new ClusterMetadata(partitioner); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(initial).withDefaultListeners(addListeners); + log = LocalLog.sync(logSpec); + log.ready(); + + service = new ClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + processorFactory.apply(log), + Commit.Replicator.NO_OP, + true); + + ClusterMetadataService.setInstance(service); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + service.commit(new Initialize(ClusterMetadata.current()) { + public Result execute(ClusterMetadata prev) + { + ClusterMetadata next = baseState; + DistributedSchema initialSchema = new DistributedSchema(prev.schema.getKeyspaces()); + ClusterMetadata.Transformer transformer = next.transformer().with(initialSchema); + return Transformation.success(transformer, EntireRange.affectedRanges(prev)); + } + + }); + service.commit(new AlterSchema((cm) -> { + return cm.schema.getKeyspaces().with(Keyspaces.of(KeyspaceMetadata.create("test", rf.asKeyspaceParams()))); + }, schemaProvider)); + } + + public void close() throws Exception + { + ClusterMetadataService.unsetInstance(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ClusterMetadataTestHelper.java b/test/distributed/org/apache/cassandra/distributed/test/log/ClusterMetadataTestHelper.java new file mode 100644 index 0000000000..5af25a18b5 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ClusterMetadataTestHelper.java @@ -0,0 +1,1078 @@ +/* + * 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.log; + +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import org.apache.cassandra.ServerTestUtils.ResettableClusterMetadataService; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.statements.schema.CreateKeyspaceStatement; +import org.apache.cassandra.cql3.statements.schema.KeyspaceAttributes; +import org.apache.cassandra.dht.ByteOrderedPartitioner; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaTransformation; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.sequences.Move; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.transformations.AlterSchema; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareMove; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Throwables; + +import static org.junit.Assert.assertEquals; + +public class ClusterMetadataTestHelper +{ + public static final InetAddressAndPort node1; + public static final InetAddressAndPort broadcastAddress; + + static + { + try + { + node1 = InetAddressAndPort.getByName("127.0.1.99"); + } + catch (UnknownHostException e) + { + throw new RuntimeException("Error initializing InetAddressAndPort"); + } + broadcastAddress = FBUtilities.getBroadcastAddressAndPort(); + } + + public static void setInstanceForTest() + { + ClusterMetadataService.setInstance(instanceForTest()); + } + + /** + * Create a blank CMS which supports mark & reset for use in tests. This version takes care of initial + * CMS setup by bootstrapping the log and applying an Initialize transformation. + * @return a resettable CMS instance, to be used in a call to ClusterMetadataService::setInstance + */ + public static ClusterMetadataService instanceForTest() + { + ClusterMetadata current = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + LocalLog log = LocalLog.asyncForTests(); + ResettableClusterMetadataService service = new ResettableClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + true); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + service.commit(new Initialize(current)); + QueryProcessor.registerStatementInvalidatingListener(); + service.mark(); + return service; + } + + /** + * Create a pre-configured CMS which supports mark & reset for use in tests. This version dose not perform initial + * CMS setup, neither bootstrapping the log nor applying an Initialize transformation. It assumes that the supplied + * ClusterMetadata instance is in the state required by the specific caller. + * @return a resettable CMS instance, to be used in a call to ClusterMetadataService::setInstance + */ + public static ClusterMetadataService instanceForTest(ClusterMetadata current) + { + LocalLog log = LocalLog.asyncForTests(); + ResettableClusterMetadataService service = new ResettableClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + true); + QueryProcessor.registerStatementInvalidatingListener(); + service.mark(); + return service; + } + + public static ClusterMetadata minimalForTesting(IPartitioner partitioner) + { + return new ClusterMetadata(Epoch.EMPTY, + Period.EMPTY, + false, + partitioner, + null, + null, + null, + DataPlacements.empty(), + null, + null, + ImmutableMap.of()); + } + + public static ClusterMetadata minimalForTesting(Keyspaces keyspaces) + { + return new ClusterMetadata(Epoch.EMPTY, + Period.EMPTY, + false, + Murmur3Partitioner.instance, + new DistributedSchema(keyspaces), + null, + null, + DataPlacements.empty(), + null, + null, + ImmutableMap.of()); + } + public static void forceCurrentPeriodTo(long period) + { + ClusterMetadata metadata = ClusterMetadata.currentNullable(); + if (metadata == null) + metadata = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + + metadata = new ClusterMetadata(metadata.epoch, + period, + metadata.lastInPeriod, + metadata.partitioner, + metadata.schema, + metadata.directory, + metadata.tokenMap, + metadata.placements, + metadata.lockedRanges, + metadata.inProgressSequences, + metadata.extensions); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(instanceForTest(metadata)); + } + + public static ClusterMetadataService syncInstanceForTest() + { + LocalLog log = LocalLog.sync(new LocalLog.LogSpec()); + log.ready(); + return new ClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + true); + } + + public static void createKeyspace(String name, KeyspaceParams params) + { + KeyspaceAttributes attributes = new KeyspaceAttributes(); + attributes.addProperty(KeyspaceParams.Option.REPLICATION.toString(), params.replication.asMap()); + CreateKeyspaceStatement createKeyspaceStatement = new CreateKeyspaceStatement(name, attributes, false); + try + { + commit(new AlterSchema(createKeyspaceStatement, Schema.instance)); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static void createKeyspace(String statement) + { + CreateKeyspaceStatement createKeyspaceStatement = (CreateKeyspaceStatement) QueryProcessor.parseStatement(statement).prepare(ClientState.forInternalCalls()); + try + { + commit(new AlterSchema(createKeyspaceStatement, Schema.instance)); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + + private static Set leaving(ClusterMetadata metadata) + { + return metadata.directory.states.entrySet().stream() + .filter(e -> e.getValue() == NodeState.LEAVING) + .map(e -> metadata.directory.endpoint(e.getKey())) + .collect(Collectors.toSet()); + } + + public static Map bootstrapping(ClusterMetadata metadata) + { + return metadata.directory.states.entrySet().stream() + .filter(e -> e.getValue() == NodeState.BOOTSTRAPPING) + .collect(Collectors.toMap(e -> metadata.tokenMap.tokens(e.getKey()).iterator().next(), + e -> metadata.directory.endpoint(e.getKey()))); + } + + public static NodeId register(int nodeIdx) + { + return register(nodeIdx, "dc0", "rack0"); + } + + public static NodeId register(InetAddressAndPort addr) + { + return register(addr, "dc0", "rack0"); + } + + public static NodeId nodeId(int nodeIdx) + { + return ClusterMetadata.current().directory.peerId(addr(nodeIdx)); + } + + public static InetAddressAndPort addr(int nodeIdx) + { + try + { + return InetAddressAndPort.getByName("127.0.0." + nodeIdx); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public static NodeAddresses addr(InetAddressAndPort address) + { + return new NodeAddresses(address); + } + + public static NodeId register(int nodeIdx, String dc, String rack) + { + return register(addr(nodeIdx), dc, rack); + } + + public static NodeId register(InetAddressAndPort endpoint, String dc, String rack) + { + try + { + return commit(new Register(addr(endpoint), new Location(dc, rack), NodeVersion.CURRENT)).directory.peerId(endpoint); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static void join(int nodeIdx, long token) + { + join(addr(nodeIdx), Collections.singleton(new Murmur3Partitioner.LongToken(token))); + } + + public static void join(InetAddressAndPort addr, Token token) + { + join(addr, Collections.singleton(token)); + } + + public static void join(InetAddressAndPort addr, Collection tokens) + { + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(addr); + JoinProcess process = lazyJoin(addr, tokens); + process.prepareJoin() + .startJoin() + .midJoin() + .finishJoin(); + + assert ClusterMetadata.current().inProgressSequences.get(nodeId) == null; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static void joinPartially(InetAddressAndPort addr, Token token) + { + joinPartially(addr, Collections.singleton(token)); + } + + public static void joinPartially(InetAddressAndPort addr, Set tokens) + { + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(addr); + JoinProcess process = lazyJoin(addr, tokens); + process.prepareJoin() + .startJoin() + .midJoin(); + + assert ClusterMetadata.current().inProgressSequences.get(nodeId) != null; + assert ClusterMetadata.current().directory.peerState(nodeId) == NodeState.BOOTSTRAPPING; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static void movePartially(InetAddressAndPort addr, Set newTokens) + { + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(addr); + // for now we preserve the constraint that can only move when non-vnodes + assertEquals(1, ClusterMetadata.current().tokenMap.tokens(nodeId).size()); + assertEquals(1, newTokens.size()); + MoveProcess process = lazyMove(addr, newTokens); + process.prepareMove() + .startMove(); + + assert ClusterMetadata.current().inProgressSequences.get(nodeId) != null; + assert ClusterMetadata.current().directory.peerState(nodeId) == NodeState.MOVING; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static void leave(int nodeIdx) + { + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(InetAddressAndPort.getByName("127.0.0." + nodeIdx)); + LeaveProcess process = lazyLeave(nodeIdx, false); + process.prepareLeave() + .startLeave() + .midLeave() + .finishLeave(); + + assert ClusterMetadata.current().inProgressSequences.get(nodeId) == null; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static JoinProcess lazyJoin(int nodeIdx, long token) + { + return lazyJoin(addr(nodeIdx), Collections.singleton(new Murmur3Partitioner.LongToken(token))); + } + + public static JoinProcess lazyJoin(InetAddressAndPort endpoint, Token token) + { + return lazyJoin(endpoint, Collections.singleton(token)); + } + + public static JoinProcess lazyJoin(InetAddressAndPort endpoint, Collection tokens) + { + return new JoinProcess() + { + int idx = 0; + + public JoinProcess prepareJoin() + { + assert idx == 0; + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(endpoint); + commit(new PrepareJoin(nodeId, + Sets.newHashSet(tokens), + ClusterMetadataService.instance().placementProvider(), + true, + false)); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public JoinProcess startJoin() + { + assert idx == 1; + try + { + BootstrapAndJoin plan = getBootstrapPlan(endpoint); + assert plan.next == Transformation.Kind.START_JOIN; + commit(plan.startJoin); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public JoinProcess midJoin() + { + assert idx == 2; + try + { + BootstrapAndJoin plan = getBootstrapPlan(endpoint); + assert plan.next == Transformation.Kind.MID_JOIN; + commit(plan.midJoin); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public JoinProcess finishJoin() + { + assert idx == 3; + try + { + BootstrapAndJoin plan = getBootstrapPlan(endpoint); + assert plan.next == Transformation.Kind.FINISH_JOIN; + commit(plan.finishJoin); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + }; + } + + public static LeaveProcess lazyLeave(int nodeIdx) + { + try + { + return lazyLeave(InetAddressAndPort.getByName("127.0.0." + nodeIdx)); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public static LeaveProcess lazyLeave(InetAddressAndPort endpoint) + { + return lazyLeave(endpoint, false); + } + + public static LeaveProcess lazyLeave(int idx, boolean force) + { + return lazyLeave(addr(idx), force); + } + + public static LeaveProcess lazyLeave(InetAddressAndPort endpoint, boolean force) + { + return new LeaveProcess() + { + int idx = 0; + public LeaveProcess prepareLeave() + { + assert idx == 0; + try + { + NodeId nodeId = ClusterMetadata.current().directory.peerId(endpoint); + commit(new PrepareLeave(nodeId, + force, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + idx++; + return this; + } + catch (Throwable e) + { + throw Throwables.throwAsUncheckedException(e); + } + } + + public LeaveProcess startLeave() + { + assert idx == 1; + try + { + UnbootstrapAndLeave plan = getLeavePlan(endpoint); + assert plan.next == Transformation.Kind.START_LEAVE; + commit(plan.startLeave); + idx++; + return this; + } + catch (Throwable e) + { + throw Throwables.throwAsUncheckedException(e); + } + } + + public LeaveProcess midLeave() + { + assert idx == 2; + try + { + UnbootstrapAndLeave plan = getLeavePlan(endpoint); + assert plan.next == Transformation.Kind.MID_LEAVE; + commit(plan.midLeave); + idx++; + return this; + } + catch (Throwable e) + { + throw Throwables.throwAsUncheckedException(e); + } + } + + public LeaveProcess finishLeave() + { + assert idx == 3; + try + { + UnbootstrapAndLeave plan = getLeavePlan(endpoint); + assert plan.next == Transformation.Kind.FINISH_LEAVE; + commit(plan.finishLeave); + idx++; + return this; + } + catch (Throwable e) + { + throw Throwables.throwAsUncheckedException(e); + } + } + }; + } + + public static void replace(int replaced, int replacement) + { + replace(addr(replaced), addr(replacement)); + } + + public static void replace(InetAddressAndPort replaced, InetAddressAndPort replacement) + { + try + { + NodeId replacementId = ClusterMetadata.current().directory.peerId(replacement); + ReplaceProcess process = lazyReplace(replaced, replacement); + process.prepareReplace() + .startReplace() + .midReplace() + .finishReplace(); + + assert ClusterMetadata.current().inProgressSequences.get(replacementId) == null; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public static ReplaceProcess lazyReplace(int replaced, int replacement) + { + return lazyReplace(addr(replaced), addr(replacement)); + } + + public static ReplaceProcess lazyReplace(InetAddressAndPort replaced, InetAddressAndPort replacement) + { + return new ReplaceProcess() + { + int idx = 0; + + public ReplaceProcess prepareReplace() + { + assert idx == 0; + try + { + NodeId replacedId = ClusterMetadata.current().directory.peerId(replaced); + NodeId replacementId = ClusterMetadata.current().directory.peerId(replacement); + commit(new PrepareReplace(replacedId, + replacementId, + ClusterMetadataService.instance().placementProvider(), + true, + false)); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public ReplaceProcess startReplace() + { + assert idx == 1; + try + { + BootstrapAndReplace plan = getReplacePlan(replacement); + assert plan.next == Transformation.Kind.START_REPLACE; + commit(plan.startReplace); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public ReplaceProcess midReplace() + { + assert idx == 2; + try + { + BootstrapAndReplace plan = getReplacePlan(replacement); + assert plan.next == Transformation.Kind.MID_REPLACE; + commit(plan.midReplace); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public ReplaceProcess finishReplace() + { + assert idx == 3; + try + { + BootstrapAndReplace plan = getReplacePlan(replacement); + assert plan.next == Transformation.Kind.FINISH_REPLACE; + commit(plan.finishReplace); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + }; + } + + public static MoveProcess lazyMove(InetAddressAndPort endpoint, Set tokens) + { + return new MoveProcess() + { + int idx = 0; + + public MoveProcess prepareMove() + { + assert idx == 0; + try + { + NodeId id = ClusterMetadata.current().directory.peerId(endpoint); + commit(new PrepareMove(id, + tokens, + ClusterMetadataService.instance().placementProvider(), + false)); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public MoveProcess startMove() + { + assert idx == 1; + try + { + Move plan = getMovePlan(endpoint); + assert plan.next == Transformation.Kind.START_MOVE; + commit(plan.startMove); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public MoveProcess midMove() + { + assert idx == 2; + try + { + Move plan = getMovePlan(endpoint); + assert plan.next == Transformation.Kind.MID_MOVE; + commit(plan.midMove); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + public MoveProcess finishMove() + { + assert idx == 3; + try + { + Move plan = getMovePlan(endpoint); + assert plan.next == Transformation.Kind.FINISH_MOVE; + commit(plan.finishMove); + idx++; + return this; + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + }; + } + + public static void addEndpoint(int i) + { + try + { + addEndpoint(InetAddressAndPort.getByName("127.0.0." + i), new Murmur3Partitioner.LongToken(i)); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public static void addEndpoint(InetAddressAndPort endpoint, Token t) + { + addEndpoint(endpoint, t, "dc1", "rack1"); + } + + public static void addEndpoint(InetAddressAndPort endpoint, Collection tokens) + { + addEndpoint(endpoint, tokens, "dc1", "rack1"); + } + + public static void addEndpoint(InetAddressAndPort endpoint, Token t, Location location) + { + addEndpoint(endpoint, Collections.singleton(t), location.datacenter, location.rack); + } + + public static void addEndpoint(InetAddressAndPort endpoint, Token t, String dc, String rack) + { + addEndpoint(endpoint, Collections.singleton(t), dc, rack); + } + + public static void addEndpoint(InetAddressAndPort endpoint, Collection t, String dc, String rack) + { + try + { + Location l = new Location(dc, rack); + commit(new Register(addr(endpoint), l, NodeVersion.CURRENT)); + lazyJoin(endpoint, new HashSet<>(t)).prepareJoin() + .startJoin() + .midJoin() + .finishJoin(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static void removeEndpoint(InetAddressAndPort endpoint, boolean force) + { + lazyLeave(endpoint, force) + .prepareLeave() + .startLeave() + .midLeave() + .finishLeave(); + } + + public static void addOrUpdateKeyspace(KeyspaceMetadata keyspace) + { + try + { + SchemaTransformation transformation = (cm) -> cm.schema.getKeyspaces().withAddedOrUpdated(keyspace); + commit(new AlterSchema(transformation, Schema.instance)); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static ClusterMetadata commit(Transformation transform) throws ExecutionException, InterruptedException + { + return ClusterMetadataService.instance().commit(transform); + } + + public static interface NodeOperations + { + public void register(); + public void join(); + public void leave(); + public JoinProcess lazyJoin(); + public LeaveProcess lazyLeave(); + } + + public static interface JoinProcess + { + public JoinProcess prepareJoin(); + public JoinProcess startJoin(); + public JoinProcess midJoin(); + public JoinProcess finishJoin(); + } + + public static interface LeaveProcess + { + public LeaveProcess prepareLeave(); + public LeaveProcess startLeave(); + public LeaveProcess midLeave(); + public LeaveProcess finishLeave(); + } + + public static interface ReplaceProcess + { + public ReplaceProcess prepareReplace(); + public ReplaceProcess startReplace(); + public ReplaceProcess midReplace(); + public ReplaceProcess finishReplace(); + } + + public static interface MoveProcess + { + public MoveProcess prepareMove(); + public MoveProcess startMove(); + public MoveProcess midMove(); + public MoveProcess finishMove(); + } + + public static VersionedEndpoints.ForToken getNaturalReplicasForToken(String keyspace, Token searchPosition) + { + return getNaturalReplicasForToken(ClusterMetadata.current(), keyspace, searchPosition); + } + + public static PrepareJoin prepareJoin(int idx) + { + return prepareJoin(nodeId(idx)); + } + public static PrepareJoin prepareJoin(NodeId nodeId) + { + return new PrepareJoin(nodeId, + Collections.singleton(Murmur3Partitioner.instance.getRandomToken()), + new UniformRangePlacement(), + true, + false); + } + + public static PrepareReplace prepareReplace(int replaced, int replacement) + { + return prepareReplace(nodeId(replaced), nodeId(replacement)); + } + + public static PrepareReplace prepareReplace(NodeId replaced, NodeId replacement) + { + return new PrepareReplace(replaced, + replacement, + new UniformRangePlacement(), + true, + false); + } + + public static PrepareLeave prepareLeave(int idx) + { + return prepareLeave(nodeId(idx)); + } + public static PrepareLeave prepareLeave(NodeId nodeId) + { + return new PrepareLeave(nodeId, + false, + new UniformRangePlacement(), + LeaveStreams.Kind.UNBOOTSTRAP); + } + + public static PrepareMove prepareMove(NodeId id, Token newToken) + { + return new PrepareMove(id, + Collections.singleton(Murmur3Partitioner.instance.getRandomToken()), + new UniformRangePlacement(), + false); + } + + /** + * get the (possibly cached) endpoints that should store the given Token. + * Note that while the endpoints are conceptually a Set (no duplicates will be included), + * we return a List to avoid an extra allocation when sorting by proximity later + * + * @param searchPosition the position the natural endpoints are requested for + * @return a copy of the natural endpoints for the given token + */ + public static VersionedEndpoints.ForToken getNaturalReplicasForToken(ClusterMetadata metadata, String keyspace, Token searchPosition) + { + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspace); + return metadata.placements.get(keyspaceMetadata.params.replication).reads.forToken(searchPosition); + } + + public static BootstrapAndJoin getBootstrapPlan(int idx) + { + return getBootstrapPlan(addr(idx)); + } + + public static BootstrapAndJoin getBootstrapPlan(InetAddressAndPort addr) + { + return getBootstrapPlan(ClusterMetadata.current().directory.peerId(addr)); + } + + public static BootstrapAndJoin getBootstrapPlan(NodeId nodeId) + { + return (BootstrapAndJoin) ClusterMetadata.current().inProgressSequences.get(nodeId); + } + + public static BootstrapAndJoin getBootstrapPlan(NodeId nodeId, ClusterMetadata metadata) + { + return (BootstrapAndJoin) metadata.inProgressSequences.get(nodeId); + } + + public static UnbootstrapAndLeave getLeavePlan(int peer) + { + return getLeavePlan(addr(peer)); + } + + public static UnbootstrapAndLeave getLeavePlan(InetAddressAndPort addr) + { + return getLeavePlan(ClusterMetadata.current().directory.peerId(addr)); + } + + public static UnbootstrapAndLeave getLeavePlan(NodeId nodeId) + { + return (UnbootstrapAndLeave) ClusterMetadata.current().inProgressSequences.get(nodeId); + } + + public static BootstrapAndReplace getReplacePlan(int idx) + { + return getReplacePlan(addr(idx)); + } + + public static BootstrapAndReplace getReplacePlan(InetAddressAndPort addr) + { + return getReplacePlan(ClusterMetadata.current().directory.peerId(addr)); + } + + public static BootstrapAndReplace getReplacePlan(NodeId nodeId) + { + return (BootstrapAndReplace) ClusterMetadata.current().inProgressSequences.get(nodeId); + } + + public static BootstrapAndReplace getReplacePlan(NodeId nodeId, ClusterMetadata metadata) + { + return (BootstrapAndReplace) metadata.inProgressSequences.get(nodeId); + } + + public static Move getMovePlan(InetAddressAndPort addr) + { + return getMovePlan(ClusterMetadata.current().directory.peerId(addr)); + } + + public static Move getMovePlan(NodeId nodeId) + { + return (Move) ClusterMetadata.current().inProgressSequences.get(nodeId); + } + + public static Move getMovePlan(NodeId nodeId, ClusterMetadata metadata) + { + return (Move) metadata.inProgressSequences.get(nodeId); + } + + public static Token bytesToken(int token) + { + return new ByteOrderedPartitioner.BytesToken(ByteBufferUtil.bytes(token)); + } + + private static final Random random = new Random(); + public static int randomInt() + { + return randomInt(Integer.MAX_VALUE); + } + + public static int randomInt(int max) + { + return random.nextInt(max); + } + + public static ListenableFuture registerOutgoingMessageSink(Verb... ignored) + { + final SettableFuture future = SettableFuture.create(); + Set ignore = Sets.newHashSet(ignored); + MessagingService.instance().outboundSink.clear(); + MessagingService.instance().outboundSink.add((Message message, InetAddressAndPort to) -> + { + if (!ignore.contains(message.verb())) + future.set(new MessageDelivery(message, to)); + return true; + }); + + MessagingService.instance().inboundSink.clear(); + MessagingService.instance().inboundSink.add((Message message) -> false); + + return future; + } + + public static class MessageDelivery + { + public final Message message; + public final InetAddressAndPort to; + + MessageDelivery(Message message, InetAddressAndPort to) + { + this.message = message; + this.to = to; + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ConflictingAddressRestartTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ConflictingAddressRestartTest.java new file mode 100644 index 0000000000..253a417fb3 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ConflictingAddressRestartTest.java @@ -0,0 +1,85 @@ +/* + * 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.log; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.junit.Ignore; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.TestBaseImpl; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.junit.Assert.assertTrue; + +public class ConflictingAddressRestartTest extends TestBaseImpl +{ + @Test + public void testRegisterWithConflictingAddress() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(builder() + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) + .withConfig(config -> config.with(GOSSIP)) + .withNodes(2).start())) + { + cluster.get(2).shutdown().get(); + + IInstanceConfig config = cluster.newInstanceConfig(); + config.set("listen_address", cluster.get(2).config().get("listen_address")); + IInvokableInstance newInstance = cluster.bootstrap(config); + try + { + newInstance.startup(); + } + catch (Exception e) + { + assertTrue(String.format("Message " + e.getMessage()), + e.getMessage().contains("conflicts with existing node")); + } + } + } + + @Test + @Ignore //todo; fails due to not being able to restart nodes, figure that out + public void testStartupWithConflictingAddress() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = init(builder() + .withConfig(config -> config.with(GOSSIP)) + .withNodes(2).start())) + { + cluster.get(2).shutdown().get(); + cluster.get(2).config().set("listen_address", cluster.get(1).config().get("listen_address")); + try + { + cluster.get(2).startup(); + } + catch (Exception e) + { + assertTrue(e.getMessage().contains("conflicts with existing node")); + } + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java new file mode 100644 index 0000000000..09b11916a4 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java @@ -0,0 +1,154 @@ +/* + * 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.log; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Assert; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.model.sut.SystemUnderTest; +import harry.visitors.*; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.fuzz.InJvmSutBase; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.ReplicationFactor; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.PrepareLeave; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.getClusterMetadataVersion; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; +import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.junit.Assert.assertFalse; + +public class ConsistentLeaveTest extends FuzzTestBase +{ + private static int WRITES = 2000; + + @Test + public void decommissionTest() throws Throwable + { + Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build()); + + try (Cluster cluster = builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + IInvokableInstance leavingInstance = cluster.get(2); + waitForCMSToQuiesce(cluster, cmsInstance); + + configBuilder.setSUT(() -> new InJvmSut(cluster, () -> 1, InJvmSutBase.retryOnTimeout())); + Run run = configBuilder.build().createRun(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + waitForCMSToQuiesce(cluster, cmsInstance); + + QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2)); + Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + model.validateAll(); + // Prime the CMS node to pause before the finish leave event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareLeave.FinishLeave); + new Thread(() -> leavingInstance.runOnInstance(() -> StorageService.instance.decommission(true))).start(); + pending.call(); + + waitForCMSToQuiesce(cluster, cmsInstance); + assertGossipStatus(cluster, leavingInstance.config().num(), "LEAVING"); + visitor = new GeneratingVisitor(run, new MutatingVisitor.MutatingVisitExecutor(run, new MutatingRowVisitor(run), SystemUnderTest.ConsistencyLevel.ALL)); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + model.validateAll(); + + // Make sure there can be only one FinishLeave in flight + waitForCMSToQuiesce(cluster, cmsInstance); + // set expectation of finish leave & retrieve the sequence when it gets committed + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + Callable finishedLeaving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareLeave.FinishLeave && r.isSuccess()); + unpauseCommits(cmsInstance); + Epoch nextEpoch = finishedLeaving.call(); + Assert.assertEquals(String.format("Epoch %s should have immediately superseded epoch %s.", nextEpoch, currentEpoch), + nextEpoch.getEpoch(), currentEpoch.getEpoch() + 1); + + // wait for the cluster to all witness the finish join event + waitForCMSToQuiesce(cluster, nextEpoch); + + assertGossipStatus(cluster, leavingInstance.config().num(), "LEFT"); + + for (int i = 0; i < WRITES; i++) + visitor.visit(); + model.validateAll(); + } + } + + private void assertGossipStatus(Cluster cluster, int leavingInstance, String status) + { + int size = cluster.size(); + List endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList()); + cluster.forEach(inst -> inst.runOnInstance(() -> { + while (true) + { + for (int i = 1; i <= size; i++) + { + String gossipStatus = Gossiper.instance.getApplicationState(endpoints.get(i - 1), ApplicationState.STATUS_WITH_PORT); + if (i != leavingInstance) + { + assertFalse(endpoints.get(i - 1) + ": " + gossipStatus, + gossipStatus.contains("LEFT")); + assertFalse(endpoints.get(i - 1) + ": " + gossipStatus, + gossipStatus.contains("LEAVING")); + } + else + { + + if (gossipStatus.contains(status)) + return; + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + } + } + } + })); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java new file mode 100644 index 0000000000..57b330adb8 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java @@ -0,0 +1,163 @@ +/* + * 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.log; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.model.sut.SystemUnderTest; +import harry.visitors.GeneratingVisitor; +import harry.visitors.LoggingVisitor; +import harry.visitors.MutatingRowVisitor; +import harry.visitors.MutatingVisitor; +import harry.visitors.Visitor; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.ReplicationFactor; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.PrepareMove; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; +import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ConsistentMoveTest extends FuzzTestBase +{ + private static int WRITES = 2000; + + @Test + public void moveTest() throws Throwable + { + Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build()); + + try (Cluster cluster = builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0")) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + IInvokableInstance movingInstance = cluster.get(2); + waitForCMSToQuiesce(cluster, cmsInstance); + + configBuilder.setSUT(() -> new InJvmSut(cluster)); + Run run = configBuilder.build().createRun(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + waitForCMSToQuiesce(cluster, cmsInstance); + + FuzzTestBase.QuiescentLocalStateChecker model = new FuzzTestBase.QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2)); + Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + model.validateAll(); + + // Make sure there can be only one FinishLeave in flight + waitForCMSToQuiesce(cluster, cmsInstance); + + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareMove.FinishMove); + new Thread(() -> { + Random rng = new Random(1); + movingInstance.runOnInstance(() -> StorageService.instance.move(Long.toString(rng.nextLong()))); + }).start(); + pending.call(); + + assertGossipStatus(cluster, movingInstance.config().num(), "MOVING"); + + // wait for the cluster to all witness the finish join event + Callable finishedMoving = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareMove.FinishMove && r.isSuccess()); + unpauseCommits(cmsInstance); + Epoch nextEpoch = finishedMoving.call(); + waitForCMSToQuiesce(cluster, nextEpoch); + + // TODO: rewrite the test to check only PENDING ranges. + visitor = new GeneratingVisitor(run, new MutatingVisitor.MutatingVisitExecutor(run, new MutatingRowVisitor(run), SystemUnderTest.ConsistencyLevel.ALL)); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + model.validateAll(); + + for (int i = 0; i < WRITES; i++) + visitor.visit(); + model.validateAll(); + + int clusterSize = cluster.size(); + List endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList()); + cluster.forEach(inst -> inst.runOnInstance(() -> { + for (int i = 1; i <= clusterSize; i++) + { + String gossipStatus = Gossiper.instance.getApplicationState(endpoints.get(i - 1), ApplicationState.STATUS_WITH_PORT); + assertTrue(endpoints.get(i - 1) + ": " + gossipStatus, + gossipStatus.contains("NORMAL")); + } + })); + } + } + + private void assertGossipStatus(Cluster cluster, int leavingInstance, String status) + { + int size = cluster.size(); + List endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList()); + cluster.forEach(inst -> inst.runOnInstance(() -> { + while (true) + { + for (int i = 1; i <= size; i++) + { + String gossipStatus = Gossiper.instance.getApplicationState(endpoints.get(i - 1), ApplicationState.STATUS_WITH_PORT); + if (i != leavingInstance) + { + assertFalse(endpoints.get(i - 1) + ": " + gossipStatus, + gossipStatus.contains("MOVING")); + } + else + { + if (gossipStatus.contains(status)) + return; + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + } + } + } + })); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java new file mode 100644 index 0000000000..398fd4ffa7 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java @@ -0,0 +1,260 @@ +/* + * 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.log; + + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.model.OpSelectors; +import harry.operations.CompiledStatement; +import harry.operations.WriteHelper; +import harry.util.ByteUtils; +import harry.util.TokenUtil; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadResponse; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.utils.concurrent.Future; + +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node; + + +public class CoordinatorPathTest extends CoordinatorPathTestBase +{ + private static final PlacementSimulator.SimpleReplicationFactor RF = new PlacementSimulator.SimpleReplicationFactor(3); + + @Test + public void writeConsistencyTest() throws Throwable + { + Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.singleRowPerModification().build()); + + coordinatorPathTest(RF, (cluster, simulatedCluster) -> { + configBuilder.setSUT(() -> new InJvmSut(cluster)); + Run run = configBuilder.build().createRun(); + + for (int ignored : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.createNode().register(); + + for (int idx : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.node(idx).join(); + + VirtualSimulatedCluster prediction = simulatedCluster.asVirtual(); + prediction.createNode(); + prediction.node(6).register(); + prediction.node(6).lazyJoin() + .prepareJoin() + .startJoin(); + + cluster.schemaChange("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};"); + cluster.schemaChange(run.schemaSpec.compile().cql()); + + while (true) + { + long lts = run.clock.nextLts(); + + long pd = run.pdSelector.pd(run.clock.nextLts(), run.schemaSpec); + + ByteBuffer[] pk = ByteUtils.objectsToBytes(run.schemaSpec.inflatePartitionKey(pd)); + long token = TokenUtil.token(ByteUtils.compose(pk)); + if (!prediction.state.get().isWriteTargetFor(token, prediction.node(6).matcher)) + continue; + + simulatedCluster.waitForQuiescense(); + List replicas = simulatedCluster.state.get().writePlacementsFor(token); + // At most 2 replicas should respond, so that when the pending node is added, results would be insufficient for recomputed blockFor + BooleanSupplier shouldRespond = atMostResponses(simulatedCluster.state.get().isWriteTargetFor(token, simulatedCluster.node(1).matcher) ? 1 : 2); + List> waiting = simulatedCluster + .filter((n) -> replicas.stream().anyMatch(n.matcher) && n.node.idx() != 1) + .map((nodeToBlockOn) -> nodeToBlockOn.blockOnReplica((node) -> new MutationAction(node, shouldRespond))) + .collect(Collectors.toList()); + + Future writeQuery = async(() -> { + long cd = run.descriptorSelector.cd(pd, lts, 0, run.schemaSpec); + CompiledStatement s = WriteHelper.inflateInsert(run.schemaSpec, + pd, + cd, + run.descriptorSelector.vds(pd, cd, lts, 0, OpSelectors.OperationKind.INSERT_WITH_STATICS, run.schemaSpec), + run.descriptorSelector.sds(pd, cd, lts, 0, OpSelectors.OperationKind.INSERT_WITH_STATICS, run.schemaSpec), + run.clock.rts(lts)); + cluster.coordinator(1).execute(s.cql(), ConsistencyLevel.QUORUM, s.bindings()); + return null; + }); + + waiting.forEach(WaitingAction::waitForMessage); + + simulatedCluster.createNode().register(); + simulatedCluster.node(6) + .lazyJoin() + .prepareJoin() + .startJoin(); + + simulatedCluster.waitForQuiescense(); + + waiting.forEach(WaitingAction::resume); + + try + { + writeQuery.get(); + Assert.fail("Should have thrown"); + } + catch (Throwable t) + { + if (t.getMessage() == null) + throw t; + Assert.assertTrue("Expected a different error message, but got " + t.getMessage(), + t.getMessage().contains("the ring has changed")); + return; + } + } + }); + } + + @Test + public void readConsistencyTest() throws Throwable + { + coordinatorPathTest(RF, (cluster, simulatedCluster) -> { + Random random = new Random(0); + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + 3 + "};", true, cluster.get(1)); + cluster.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))", true, cluster.get(1)); + + for (int ignored : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.createNode().register(); + + for (int idx : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.node(idx).join(); + + while (true) + { + int pk = random.nextInt(); + if (!simulatedCluster.state.get().isReadReplicaFor(token(pk), simulatedCluster.node(4).matcher) || + !simulatedCluster.state.get().isReadReplicaFor(token(pk), simulatedCluster.node(1).matcher)) + continue; + + simulatedCluster.waitForQuiescense(); + + List replicas = simulatedCluster.state.get().readReplicasFor(token(pk)); + Function shouldRespond = respondFrom(1, 4); + List> waiting = simulatedCluster + .filter((n) -> replicas.stream().anyMatch(n.matcher) && n.node.idx() != 1) + .map((nodeToBlockOn) -> nodeToBlockOn.blockOnReplica((node) -> new ReadAction(node, shouldRespond.apply(nodeToBlockOn.node.idx())))) + .collect(Collectors.toList()); + + Future readQuery = async(() -> cluster.coordinator(1).execute("select * from distributed_test_keyspace.tbl where pk = ?", ConsistencyLevel.QUORUM, pk)); + + waiting.forEach(WaitingAction::waitForMessage); + + simulatedCluster.node(4) + .lazyLeave() + .prepareLeave() + .startLeave() + .midLeave() + .finishLeave(); + + simulatedCluster.waitForQuiescense(); + + waiting.forEach(WaitingAction::resume); + + try + { + readQuery.get(); + Assert.fail(); + } + catch (Throwable t) + { + if (t.getMessage() == null) + throw t; + Assert.assertTrue(String.format("Got exception: %s", t), + t.getMessage().contains("the ring has changed")); + return; + } + } + }); + } + + @Test + public void coordinatorReadWriteTest() throws Throwable + { + coordinatorPathTest(RF, (cluster, simulatedCluster) -> { + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + 3 + "};"); + cluster.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + + for (int ignored : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.createNode().register(); + for (int idx : new int[]{ 2, 3, 4, 5 }) + simulatedCluster.node(idx).join(); + + simulatedCluster.createNode(); + simulatedCluster.node(6).register(); + simulatedCluster.node(6).lazyJoin() + .prepareJoin() + .startJoin(); + + simulatedCluster.waitForQuiescense(); + + AtomicInteger reads = new AtomicInteger(); + AtomicInteger writes = new AtomicInteger(); + simulatedCluster.node(6).clean(Verb.READ_REQ); + simulatedCluster.node(6).on(Verb.READ_REQ, new ReadAction(simulatedCluster.node(6)) { + public Message respondTo(Message request) + { + reads.incrementAndGet(); + return super.respondTo(request); + } + }); + simulatedCluster.node(6).clean(Verb.MUTATION_REQ); + simulatedCluster.node(6).on(Verb.MUTATION_REQ, new MutationAction(simulatedCluster.node(6)) { + public Message respondTo(Message request) + { + writes.incrementAndGet(); + return super.respondTo(request); + } + }); + int expectedWrites = 0; + for (int i = 0; i < 500; i++) + { + if (simulatedCluster.state.get().isWriteTargetFor(token(i), simulatedCluster.node(6).matcher)) + expectedWrites++; + cluster.coordinator(1).execute("insert into distributed_test_keyspace.tbl (pk, ck) values (" + i + ", 1)", ConsistencyLevel.ALL); + cluster.coordinator(1).execute("select * from distributed_test_keyspace.tbl where pk = " + i, ConsistencyLevel.ALL); + } + Assert.assertEquals(0, reads.get()); + Assert.assertEquals(expectedWrites, writes.get()); + }); + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java new file mode 100644 index 0000000000..7f4fc82b32 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java @@ -0,0 +1,1152 @@ +/* + * 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.log; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import harry.util.ByteUtils; +import harry.util.TestRunner; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadResponse; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ICluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.impl.Instance; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations; +import org.apache.cassandra.gms.GossipDigestAck; +import org.apache.cassandra.gms.GossipDigestSyn; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.SimpleSeedProvider; +import org.apache.cassandra.tcm.*; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.utils.AssertUtil; +import org.apache.cassandra.utils.Closeable; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.AsyncPromise; +import org.apache.cassandra.utils.concurrent.CountDownLatch; +import org.apache.cassandra.utils.concurrent.Future; + +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.RefSimulatedPlacementHolder; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacementHolder; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements; +import static org.apache.cassandra.net.Verb.GOSSIP_DIGEST_ACK; +import static org.apache.cassandra.net.Verb.TCM_REPLICATION; + +public abstract class CoordinatorPathTestBase extends FuzzTestBase +{ + private static final Logger logger = LoggerFactory.getLogger(CoordinatorPathTestBase.class); + + public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer test) throws Throwable + { + coordinatorPathTest(rf, test, true); + } + + /** + * Coordinator path test allows to test a real node (127.0.0.1), which is _not_ a part of CMS. + * + * CMS node is 127.0.0.10 and is simulated. + */ + public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer test, boolean startCluster) throws Throwable + { + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + + String nodeUnderTest = "127.0.0.1"; + InetAddressAndPort nodeUnderTestAddr = InetAddressAndPort.getByName(nodeUnderTest + ":7012"); + + PlacementSimulator.NodeFactory factory = PlacementSimulator.nodeFactory(); + Node fakeCmsNode = factory.make(10,1,1); + FBUtilities.setBroadcastInetAddressAndPort(fakeCmsNode.addr()); + + try (Cluster cluster = builder().withNodes(1) + .withConfig(cfg -> cfg.set("seed_provider", new ParameterizedClass(SimpleSeedProvider.class.getName(), + Collections.singletonMap("seeds", fakeCmsNode.id() + ":7012")))) + .withTokenSupplier(factory) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(10, "dc0", "rack0")) + .createWithoutStarting(); + SimulatedCluster simulatedCluster = new SimulatedCluster(rf, cluster, factory)) + { + simulatedCluster.initWithFakeCms(fakeCmsNode, nodeUnderTestAddr); + if (startCluster) + cluster.startup(); + test.accept(cluster, simulatedCluster); + } + finally + { + ClusterMetadataService.unsetInstance(); + } + } + + public void cmsNodeTest(TestRunner.ThrowingBiConsumer test) throws Throwable + { + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + + TokenSupplier tokenSupplier = TokenSupplier.evenlyDistributedTokens(10); + try (Cluster cluster = builder().withNodes(1) + .withTokenSupplier(tokenSupplier) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(10, "dc0", "rack0")) + .createWithoutStarting(); + SimulatedCluster simulatedCluster = new SimulatedCluster(new PlacementSimulator.SimpleReplicationFactor(3), + cluster, tokenSupplier)) + { + // Note that in case of a CMS node teset we first start a cluster, and then initialize a simulated cluster. + cluster.startup(); + simulatedCluster.init(); + test.accept(cluster, simulatedCluster); + } + finally + { + ClusterMetadataService.unsetInstance(); + } + } + + + @Override + public Cluster.Builder builder() { + return Cluster.build() + .withConfig(cfg -> cfg.set("request_timeout", String.format("%dms", TimeUnit.MINUTES.toMillis(10)))); + } + + public static BooleanSupplier atMostResponses(int maxResponses) + { + AtomicInteger responsesSoFar = new AtomicInteger(); + return () -> { + int responses = responsesSoFar.incrementAndGet(); + return responses <= maxResponses; + }; + } + + public static Function respondFrom(int... nodeIds) + { + Set nodes = new HashSet<>(); + for (int idx : nodeIds) + nodes.add(idx); + + return (Integer idx) -> () -> nodes.contains(idx); + } + + public static Future async(AssertUtil.ThrowingSupplier supplier) + { + AsyncPromise future = new AsyncPromise<>(); + new Thread(() -> { + try + { + future.setSuccess(supplier.get()); + } + catch (Throwable t) + { + future.setFailure(t); + } + }).start(); + return future; + } + + public static final Map, Verb> classToVerb = new HashMap<>(); + static + { + classToVerb.put(FetchCMSLog.class, Verb.TCM_FETCH_CMS_LOG_REQ); + classToVerb.put(Commit.class, Verb.TCM_COMMIT_REQ); + } + + protected static class RealSimulatedNode extends VirtualSimulatedNode implements Predicate> + { + protected final Supplier entryIdGen; + protected final EnumMap> actions; + protected final SimulatedCluster cluster; + + private RealSimulatedNode(SimulatedCluster simulatedCluster, Node node) + { + super(simulatedCluster.state, node); + + this.entryIdGen = new Entry.DefaultEntryIdGen(); + this.actions = new EnumMap<>(Verb.class); + this.cluster = simulatedCluster; + } + + public void initializeDefaultHandlers() + { + on(Verb.GOSSIP_DIGEST_SYN, new GossipSynAction(this)); + on(Verb.READ_REQ, new ReadAction(this)); + on(Verb.RANGE_REQ, new RangeReadAction(this)); + on(Verb.MUTATION_REQ, new MutationAction(this)); + on(Verb.TCM_NOTIFY_REQ, new LogNotifyAction(this)); + on(Verb.TCM_REPLICATION, new EmptyAction(this, Verb.TCM_REPLICATION)); + on(Verb.TCM_COMMIT_RSP, new EmptyAction(this, Verb.TCM_COMMIT_RSP)); + on(Verb.GOSSIP_DIGEST_ACK2, new EmptyAction(this, Verb.GOSSIP_DIGEST_ACK2)); + on(Verb.GOSSIP_SHUTDOWN, new EmptyAction(this, Verb.GOSSIP_SHUTDOWN)); + on(Verb.READ_REPAIR_REQ, new MutationAction(this)); + } + + public String toString() + { + return "RealSimulatedNode{" + + "id=" + node.idx() + + ", token=" + node.token() + + '}'; + } + + @Override + public void register() + { + super.register(); + ClusterMetadataTestHelper.register(node.idx()); + } + + @Override + public ClusterMetadataTestHelper.JoinProcess lazyJoin() + { + ClusterMetadataTestHelper.JoinProcess virtual = super.lazyJoin(); + ClusterMetadataTestHelper.JoinProcess real = ClusterMetadataTestHelper.lazyJoin(node.idx(), node.token()); + return new ClusterMetadataTestHelper.JoinProcess() + { + public ClusterMetadataTestHelper.JoinProcess prepareJoin() + { + virtual.prepareJoin(); + real.prepareJoin(); + return this; + } + + public ClusterMetadataTestHelper.JoinProcess startJoin() + { + virtual.startJoin(); + real.startJoin(); + return this; + } + + public ClusterMetadataTestHelper.JoinProcess midJoin() + { + virtual.midJoin(); + real.midJoin(); + return this; + } + + public ClusterMetadataTestHelper.JoinProcess finishJoin() + { + virtual.finishJoin(); + real.finishJoin(); + return this; + } + }; + } + + @Override + public ClusterMetadataTestHelper.LeaveProcess lazyLeave() + { + ClusterMetadataTestHelper.LeaveProcess virtual = super.lazyLeave(); + ClusterMetadataTestHelper.LeaveProcess real = ClusterMetadataTestHelper.lazyLeave(node.idx()); + return new ClusterMetadataTestHelper.LeaveProcess() + { + public ClusterMetadataTestHelper.LeaveProcess prepareLeave() + { + virtual.prepareLeave(); + real.prepareLeave(); + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess startLeave() + { + virtual.startLeave(); + real.startLeave(); + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess midLeave() + { + virtual.midLeave(); + real.midLeave(); + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess finishLeave() + { + virtual.finishLeave(); + real.finishLeave(); + return this; + } + }; + } + + public void on(Verb verb, SimulatedAction action) + { + SimulatedAction prev = actions.put(verb, action); + assert prev == null : String.format("Already had a subscribed handler for " + verb); + } + + /** + * Serialize and send a single message from this (simulated) node to the real node in the simulated cluster, + * and block/wait for its response. + */ + public OUT requestResponse(IN payload) + { + AsyncPromise resFuture = new AsyncPromise<>(); + Verb verb = classToVerb.get(payload.getClass()).responseVerb; + return withTemporaryHandler(verb, + new AbstractSimulatedAction(this) + { + public Message respondTo(Message rsp) + { + resFuture.setSuccess(rsp.payload); + return null; + } + + @Override + public Verb verb() + { + return verb; + } + }, + () -> { + sendFrom(node.idx(), payload); + try + { + return resFuture.get(1, TimeUnit.MINUTES); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + } + + public T withTemporaryHandler(Verb verb, SimulatedAction action, Callable r) + { + SimulatedAction prev = actions.put(verb, action); + T res = null; + try + { + res = r.call(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + if (prev != null) + { + SimulatedAction tmp = actions.put(verb, prev); + assert tmp == action; + } + else + { + actions.remove(verb); + } + return res; + } + + public void clean(Verb verb) + { + actions.remove(verb); + } + + /** + * Deliver a single message from this (simulated) node to the real node in the simulated cluster. Communication + * between the simulated nodes is transparent for the purposes of this test, so it was not implemented. + */ + public void sendFrom(int from, Message message) + { + try + { + cluster.realCluster.deliverMessage(cluster.realCluster.get(1).broadcastAddress(), + // todo: use addr from the node! + Instance.serializeMessage(ClusterMetadataTestHelper.addr(from), + ClusterMetadataTestHelper.addr(1), + message)); + } + catch (Throwable t) + { + throw new IllegalStateException(String.format("Caught an error while delivering %s to %d", message, from), + t); + } + } + + /** + * Serialize and deliver a single payload from this (simulated) node to the the real node in the simulated cluster. + * Communication between the simulated nodes is transparent for the purposes of this test, so it was not implemented. + */ + public void sendFrom(int from, T payload) + { + sendFrom(from, + Message.out(classToVerb.get(payload.getClass()), payload, 0)); + } + + /** + * Try delivering to one of the internally subscribed handlers + */ + @SuppressWarnings("unchecked, rawtypes") + public boolean test(Message message) + { + SimulatedAction action = actions.get(message.verb()); + Assert.assertNotNull(String.format("Can't find an action that corresponds to verb %s", message.verb()), action); + action.validate(message); + Message response = action.respondTo(message); + if (response != null) + sendFrom(response.from().addressBytes[3], response); + + return false; + } + + /** + * Executes {@param request}, + */ + public WaitingAction blockOnReplica(Function> factory) + { + WaitingAction waitingAction = new WaitingAction<>(() -> factory.apply(this)); + actions.put(waitingAction.verb(), waitingAction); + return waitingAction; + } + + } + + /** + * A virtual simulated node, that can only be used to make predictions about the cluster state, and doesn't + * add any handlers to the cluster state. + */ + protected static class VirtualSimulatedNode implements ClusterMetadataTestHelper.NodeOperations + { + public final Node node; + public final Predicate matcher; + protected final SimulatedPlacementHolder ref; + + public VirtualSimulatedNode(SimulatedPlacementHolder ref, Node node) + { + this.ref = ref; + this.node = node; + this.matcher = (o) -> node.idx() == o.idx(); + } + + @Override + public void register() + { + // TODO: mark that we've registered somewhere, it'd be good to validate that at some point + } + + @Override + public void join() + { + lazyJoin().prepareJoin() + .startJoin() + .midJoin() + .finishJoin(); + } + + @Override + public void leave() + { + lazyLeave().prepareLeave() + .startLeave() + .midLeave() + .finishLeave(); + } + + @Override + public ClusterMetadataTestHelper.JoinProcess lazyJoin() + { + return new ClusterMetadataTestHelper.JoinProcess() + { + PlacementSimulator.Transformations steps = null; + int idx = -1; + public ClusterMetadataTestHelper.JoinProcess prepareJoin() + { + assert idx == -1; + assert steps == null; + + SimulatedPlacements placements = ref.get(); + steps = PlacementSimulator.join(placements, node); + ref.set(placements.withStashed(steps)); + ref.applyNext(steps); + idx++; + return this; + } + + public ClusterMetadataTestHelper.JoinProcess startJoin() + { + assert idx == 0; + ref.applyNext(steps); + idx++; + return this; + } + + public ClusterMetadataTestHelper.JoinProcess midJoin() + { + assert idx == 1; + ref.applyNext(steps); + idx++; + return this; + } + + public ClusterMetadataTestHelper.JoinProcess finishJoin() + { + assert idx == 2; + ref.applyNext(steps); + idx++; + assert !steps.hasNext(); + return this; + } + }; + } + + @Override + public ClusterMetadataTestHelper.LeaveProcess lazyLeave() + { + return new ClusterMetadataTestHelper.LeaveProcess() + { + Transformations steps = null; + int idx = -1; + public ClusterMetadataTestHelper.LeaveProcess prepareLeave() + { + assert idx == -1; + assert steps == null; + + SimulatedPlacements placements = ref.get(); + Transformations tranformations = PlacementSimulator.leave(placements, node); + ref.set(placements.withStashed(tranformations)); + + steps = tranformations; + idx++; + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess startLeave() + { + assert idx == 0; + ref.applyNext(steps); + idx++; + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess midLeave() + { + assert idx == 1; + ref.applyNext(steps); + idx++; + return this; + } + + public ClusterMetadataTestHelper.LeaveProcess finishLeave() + { + assert idx == 2; + ref.applyNext(steps); + idx++; + return this; + } + }; + } + } + + public static class SimulatedCluster implements Closeable + { + protected final ICluster realCluster; + protected final SimulatedPlacementHolder state; + protected final Map nodes; + protected final TokenSupplier tokenSupplier; + protected final IPartitioner partitioner = Murmur3Partitioner.instance; + protected ExecutorPlus executor; + public final PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + + public SimulatedCluster(PlacementSimulator.ReplicationFactor rf, ICluster realCluster, TokenSupplier tokenSupplier) + { + this.tokenSupplier = tokenSupplier; + + InetAddressAndPort nodeUnderTestAddr = ClusterMetadataTestHelper.addr(1); + Node nodeUnderTest = nodeFactory.make(nodeUnderTestAddr.addressBytes[3], 1, 1); + List orig = Collections.singletonList(nodeUnderTest); + this.state = new RefSimulatedPlacementHolder(new SimulatedPlacements(rf, + Collections.singletonList(nodeUnderTest), + rf.replicate(orig).asMap(), + rf.replicate(orig).asMap(), + Collections.emptyList())); + this.realCluster = realCluster; + this.nodes = new HashMap<>(); + + // We would like all messages directed to the node under test to be delivered it. + this.nodes.put(nodeUnderTestAddr, new RealSimulatedNode(this, nodeUnderTest) { + @Override + public boolean test(Message message) + { + realCluster.get(1).receiveMessage(Instance.serializeMessage(message.from(), nodeUnderTestAddr, message)); + return true; + } + }); + } + + public void initWithFakeCms(Node cms, InetAddressAndPort nodeUnderTest) + { + assert executor == null; + LogStorage logStorage = new AtomicLongBackedProcessor.InMemoryStorage(); + ClusterMetadata initial = new ClusterMetadata(partitioner); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(initial).withStorage(logStorage); + LocalLog log = LocalLog.sync(logSpec); + log.ready(); + // Replicator only replicates to the node under test, as there are no other nodes in reality + Commit.Replicator replicator = (result, source) -> { + realCluster.deliverMessage(realCluster.get(1).broadcastAddress(), + Instance.serializeMessage(cms.addr(), nodeUnderTest, + Message.out(Verb.TCM_REPLICATION, result.success().replication))); + }; + + AtomicLongBackedProcessor processor = new AtomicLongBackedProcessor(log); + + ClusterMetadataService service = new ClusterMetadataService(new UniformRangePlacement(), + new AtomicLongBackedProcessor.InMemoryMetadataSnapshots(), + log, + processor, + replicator, + true); + ClusterMetadataService.setInstance(service); + + log.bootstrap(cms.addr()); + service.commit(new Initialize(log.metadata())); + service.commit(new Register(new NodeAddresses(cms.addr()), new Location(cms.dc(), cms.rack()), NodeVersion.CURRENT)); + + IVerbHandler commitRequestHandler = Commit.handlerForTests(processor, + replicator, + (msg, to) -> realCluster.deliverMessage(to, Instance.serializeMessage(cms.addr(), to, msg))); + executor = ExecutorFactory.Global.executorFactory().pooled("FakeMessaging", 10); + + realCluster.setMessageSink((target, msg) -> executor.submit(() -> { + try + { + Message message = Instance.deserializeMessage(msg); + // Catch the messages from the node under test and forward them to the CMS + if (target.equals(cms.addr())) + { + switch (message.verb()) + { + case TCM_DISCOVER_REQ: + Message rsp = message.responseWith(new Discovery.DiscoveredNodes(Collections.singleton(cms.addr()), Discovery.DiscoveredNodes.Kind.CMS_ONLY)); + realCluster.deliverMessage(message.from(), + Instance.serializeMessage(cms.addr(), message.from(), rsp)); + return; + case TCM_COMMIT_REQ: + { + commitRequestHandler.doVerb((Message) message); + return; + } + case TCM_FETCH_CMS_LOG_REQ: + { + FetchCMSLog request = (FetchCMSLog) message.payload; + LogState logState = logStorage.getLogState(request.lowerBound); + realCluster.deliverMessage(message.from(), + Instance.serializeMessage(cms.addr(), message.from(), message.responseWith(logState))); + return; + } + default: + logger.error("Mocked CMS node has received message with {} verb: {}", msg.verb(), msg); + } + } + else + { + nodes.get(target).test(message); + } + } + catch (Throwable t) + { + logger.error(String.format("Caught an exception while trying to deliver %s to %s", msg, target), t); + } + })); + } + + public void init() + { + assert executor == null; + + // We need to create a second node to be able to send and receive messages. + RealSimulatedNode driver = createNode(); + LocalLog log = LocalLog.sync(new LocalLog.LogSpec()); + log.ready(); + + ClusterMetadataService metadataService = + new ClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new Processor() + { + public Commit.Result commit(Entry.Id entryId, Transformation event, Epoch lastKnown, Retry.Deadline retryPolicy) + { + if (lastKnown == null) + lastKnown = log.waitForHighestConsecutive().epoch; + Commit.Result result = driver.requestResponse(new Commit(entryId, event, lastKnown)); + if (result.isSuccess()) + { + log.append(result.success().replication.entries()); + log.waitForHighestConsecutive(); + } + return result; + } + + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + Epoch since = log.waitForHighestConsecutive().epoch; + LogState logState = driver.requestResponse(new FetchCMSLog(since, true)); + log.append(logState); + return log.waitForHighestConsecutive(); + } + }, + (a,b) -> {}, + false); + + ClusterMetadataService.setInstance(metadataService); + driver.clean(TCM_REPLICATION); + driver.on(Verb.TCM_REPLICATION, new SimulatedAction() + { + public Verb verb() + { + return TCM_REPLICATION; + } + + public void validate(Message request) + { + // no-op + } + + public Message respondTo(Message request) + { + for (Entry entry : request.payload.entries()) + log.append(entry); + log.waitForHighestConsecutive(); + return null; + } + }); + + // We're using executor for blocking messages perpetually (at least for now). It is possible + // to have a more efficient way of blocking, namely one with a queue and continuation or a singal, + // but implementing this is just not feasible until the test harness proves itself useful. + executor = ExecutorFactory.Global.executorFactory().pooled("FakeMessaging", 10); + realCluster.setMessageSink((target, msg) -> executor.submit(() -> { + try + { + nodes.get(target).test(Instance.deserializeMessage(msg)); + } + catch (Throwable t) + { + logger.error(String.format("Caught an exception while trying to deliver %s to %s", msg, target), t); + } + })); + driver.register(); + } + + public RealSimulatedNode createNode() + { + int idx = this.nodes.size() + 1; + RealSimulatedNode node = new RealSimulatedNode(this, nodeFactory.make(idx, 1, 1)); + node.initializeDefaultHandlers(); + nodes.put(node.node.addr(), node); + return node; + } + + public VirtualSimulatedCluster asVirtual() + { + return new VirtualSimulatedCluster(state.fork(), nodeFactory); + } + + public RealSimulatedNode node(int i) + { + return nodes.get(ClusterMetadataTestHelper.addr(i)); + } + + public Optional find(Predicate predicate) + { + return nodes.values().stream().filter(predicate).findFirst(); + } + + public Stream filter(Predicate predicate) + { + return nodes.values().stream().filter(predicate); + } + + public void waitForQuiescense() + { + Epoch waitFor = ClusterMetadataService.instance().log().waitForHighestConsecutive().epoch; + realCluster.get(1).acceptsOnInstance((Long epoch) -> { + try + { + ClusterMetadataService.instance().awaitAtLeast(Epoch.create(epoch)); + } + catch (InterruptedException | TimeoutException e) + { + throw new RuntimeException(e); + } + }).accept(waitFor.getEpoch()); + } + + public void close() + { + executor.shutdownNow(); + } + } + + public static class VirtualSimulatedCluster + { + protected final SimulatedPlacementHolder state; + protected final List nodes; + protected final PlacementSimulator.NodeFactory factory; + + public VirtualSimulatedCluster(SimulatedPlacementHolder state, PlacementSimulator.NodeFactory factory) + { + this.state = state; + this.factory = factory; + this.nodes = new ArrayList<>(); + for (Node node : state.get().nodes) + this.nodes.add(new VirtualSimulatedNode(state, node)); + } + + public VirtualSimulatedNode createNode() + { + int idx = nodes.size() + 1; + VirtualSimulatedNode node = new VirtualSimulatedNode(state, factory.make(idx, 1, 1)); + nodes.add(node); + return node; + } + + public VirtualSimulatedNode node(int idx) + { + return nodes.get(idx - 1); + } + } + + public interface SimulatedAction + { + Verb verb(); + void validate(Message request); + // Null returned by this method means we don't want to send a response + Message respondTo(Message request); + } + + public static abstract class AbstractSimulatedAction implements SimulatedAction + { + protected final RealSimulatedNode node; + + public AbstractSimulatedAction(RealSimulatedNode node) + { + this.node = node; + } + + public void validate(Message request) {} + public abstract Message respondTo(Message request); + } + + + public static class GossipSynAction extends AbstractSimulatedAction + { + public GossipSynAction(RealSimulatedNode node) + { + super(node); + } + + @Override + public Verb verb() + { + return Verb.GOSSIP_DIGEST_SYN; + } + + @Override + public void validate(Message request) + { + // no-op + } + + @Override + public Message respondTo(Message request) + { + return Message.out(GOSSIP_DIGEST_ACK, new GossipDigestAck(Collections.emptyList(), Collections.emptyMap())); + } + } + + public static class EmptyAction extends AbstractSimulatedAction + { + private final Verb verb; + public EmptyAction(RealSimulatedNode node, Verb verb) + { + super(node); + this.verb = verb; + } + + @Override + public Verb verb() + { + return verb; + } + + @Override + public void validate(Message request) + { + } + + @Override + public Message respondTo(Message request) + { + return null; + } + } + + public static class LogNotifyAction extends AbstractSimulatedAction + { + public LogNotifyAction(RealSimulatedNode node) + { + super(node); + } + + @Override + public Verb verb() + { + return Verb.TCM_NOTIFY_REQ; + } + + @Override + public Message respondTo(Message request) + { + return request.emptyResponse(); + } + } + + public static class ReadAction extends AbstractSimulatedAction + { + private BooleanSupplier shouldRespond; + + public ReadAction(RealSimulatedNode node) + { + this(node, () -> true); + } + + public ReadAction(RealSimulatedNode node, BooleanSupplier shouldRespond) + { + super(node); + this.shouldRespond = shouldRespond; + } + + @Override + public Verb verb() + { + return Verb.READ_REQ; + } + + @Override + public void validate(Message request) + { + ReadCommand command = request.payload; + assert command instanceof SinglePartitionReadCommand; + SinglePartitionReadCommand sprc = (SinglePartitionReadCommand) command; + + Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) sprc.partitionKey().getToken(); + assert node.cluster.state.get().isReadReplicaFor(requestToken.token, node.matcher) : + String.format("Node %s is not a read replica for %s. Replicas: %s", + node, requestToken.token, node.cluster.state.get().readReplicasFor(requestToken.token)); + } + + @Override + public Message respondTo(Message request) + { + if (shouldRespond.getAsBoolean()) + { + ReadCommand command = request.payload; + return Message.remoteResponseForTests(request.id(), + node.node.addr(), + request.verb().responseVerb, + ReadResponse.createDataResponse(EmptyIterators.unfilteredPartition(command.metadata()), + command)); + } + return null; + } + } + + public static class RangeReadAction extends AbstractSimulatedAction + { + public RangeReadAction(RealSimulatedNode node) + { + super(node); + } + + @Override + public Verb verb() + { + return Verb.RANGE_REQ; + } + + @Override + public void validate(Message request) + { + ReadCommand command = request.payload; + assert command instanceof PartitionRangeReadCommand; + PartitionRangeReadCommand prrc = (PartitionRangeReadCommand) command; + + Murmur3Partitioner.LongToken leftToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().left.getToken(); + Murmur3Partitioner.LongToken rightToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().right.getToken(); + assert node.cluster.state.get().isReadReplicaFor(leftToken.token, rightToken.token, node.matcher); + } + + @Override + public Message respondTo(Message request) + { + ReadCommand command = request.payload; + return request.responseWith(ReadResponse.createDataResponse(EmptyIterators.unfilteredPartition(command.metadata()), + command)); + } + } + + public static class MutationAction extends AbstractSimulatedAction + { + private BooleanSupplier shouldRespond; + + public MutationAction(RealSimulatedNode node) + { + this(node, () -> true); + } + + @Override + public Verb verb() + { + return Verb.MUTATION_REQ; + } + + public MutationAction(RealSimulatedNode node, BooleanSupplier shouldRespond) + { + super(node); + this.shouldRespond = shouldRespond; + } + + public void validate(Message request) + { + Mutation command = request.payload; + Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) command.key().getToken(); + assert node.cluster.state.get().isWriteTargetFor(requestToken.token, node.matcher) : String.format("Node %s is not a write target for %s. Write placements: %s", + node.node.idx(), requestToken, node.cluster.state.get().writePlacementsFor(requestToken.token)); + } + + public Message respondTo(Message request) + { + if (shouldRespond.getAsBoolean()) + return Message.remoteResponseForTests(request.id(), node.node.addr(), request.verb().responseVerb, NoPayload.noPayload); + return null; + } + } + + /** + * Waiting action is a way to block replicas right before responding to the coordinator. + */ + public static class WaitingAction implements SimulatedAction + { + // TODO: switch from latches to signals + private final CountDownLatch gotAtLeastOneMessage; + private final CountDownLatch releaseWaitingThread; + private final SimulatedAction action; + + public WaitingAction(Supplier> factory) + { + this.action = factory.get(); + this.gotAtLeastOneMessage = CountDownLatch.newCountDownLatch(1); + this.releaseWaitingThread = CountDownLatch.newCountDownLatch(1); + } + + @Override + public Verb verb() + { + return action.verb(); + } + + public void validate(Message request) + { + action.validate(request); + } + + public Message respondTo(Message request) + { + gotAtLeastOneMessage.decrement(); + releaseWaitingThread.awaitUninterruptibly(); + return action.respondTo(request); + } + + public void waitForMessage() + { + gotAtLeastOneMessage.awaitUninterruptibly(); + } + + public void resume() + { + releaseWaitingThread.decrement(); + } + } + + public static long token(Object... pk) + { + return ((Murmur3Partitioner.LongToken) DatabaseDescriptor.getPartitioner().decorateKey(toBytes(pk)).getToken()).token; + } + + public static ByteBuffer toBytes(Object... pk) + { + assert pk.length > 0; + if (pk.length == 1) + return ByteUtils.objectToBytes(pk[0]); + ByteBuffer[] bbs = new ByteBuffer[pk.length]; + for (int i = 0; i < pk.length; i++) + { + bbs[i] = ByteUtils.objectToBytes(pk[i]); + } + return ByteUtils.compose(bbs); + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/DiscoverCMSTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/DiscoverCMSTest.java new file mode 100644 index 0000000000..d3a4f420a2 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/DiscoverCMSTest.java @@ -0,0 +1,84 @@ +/* + * 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.log; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.RemoteProcessor; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +public class DiscoverCMSTest extends TestBaseImpl +{ + @Test + public void discoverTest() throws IOException, TimeoutException + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .withInstanceInitializer(BB::install) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + // Simulate node2 having an incorrect view of the CMS membership. This triggers a discovery request and + // potential retry which can be observed in the logs. + cluster.get(2).runOnInstance(() -> BB.returnIncorrectList.set(true)); + cluster.coordinator(2).execute(withKeyspace("create table %s.tbl2 (id int primary key)"), ConsistencyLevel.ONE); + cluster.get(2).logs().watchFor("/127.0.0.3:7012 is not a member of the CMS, querying it to discover current membership"); + cluster.get(2).logs().watchFor("Got CMS from /127.0.0.3:7012: DiscoveredNodes\\{nodes=\\[/127.0.0.1:7012\\], kind=CMS_ONLY\\}, retrying on: CandidateIterator\\{candidates=\\[/127.0.0.1:7012, /127.0.0.3:7012\\], checkLive=true}"); + } + } + + public static class BB + { + public static AtomicBoolean returnIncorrectList = new AtomicBoolean(false); + public static void install(ClassLoader cl, int i) + { + if (i != 2) + return; + new ByteBuddy().rebase(RemoteProcessor.class) + .method(named("candidates")) + .intercept(MethodDelegation.to(BB.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + + public static List candidates(boolean allowDiscovery) + { + InetAddressAndPort cms = InetAddressAndPort.getByNameUnchecked("127.0.0.1"); + if (returnIncorrectList.get()) + cms = InetAddressAndPort.getByNameUnchecked("127.0.0.3"); + return Arrays.asList(cms); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/DistributedLogTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/DistributedLogTest.java new file mode 100644 index 0000000000..d37c3a665b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/DistributedLogTest.java @@ -0,0 +1,200 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DistributedLogTest extends TestBaseImpl +{ + @Test + public void testSequentialDelivery() throws Throwable + { + try (Cluster cluster = builder().withDC("DC1", 2) + .withDC("DC2", 2) + .withConfig(config -> config.with(NETWORK, GOSSIP).set("metadata_snapshot_frequency", Integer.MAX_VALUE)) // test assumes that all nodes have all events, that is not true if snapshotting + // todo; add config param to disable snapshotting + .start()) + { + List expected = new ArrayList<>(); + + final int iterations = 100; + for (int i = 0; i < iterations; i++) + { + for (int j = 1; j <= cluster.size(); j++) + { + int node = j; + int counter = i; + + String s = cluster.get(j).callOnInstance(() -> { + try + { + return (String) ClusterMetadataService.instance() + .commit(CustomTransformation.make(String.format("test payload %d %d", node, counter))) + .extensions.get(CustomTransformation.PokeString.METADATA_KEY).getValue(); + } + catch (Throwable t) + { + throw new AssertionError(t); + } + }); + expected.add(s); + } + } + + for (int counter = 0; counter < iterations; counter++) + { + for (int node = 1; node <= cluster.size(); node++) + { + Assert.assertTrue(expected.contains(String.format("test payload %d %d", node, counter))); + } + } + + assertEquals(iterations * cluster.size(), + expected.size()); + + cluster.forEach(node -> { + List actual = node.callOnInstance(() -> { + List res = new ArrayList<>(); + + for (Entry entry : LogStorage.SystemKeyspace.getLogState(Epoch.FIRST).transformations.entries()) + { + if (entry.transform instanceof CustomTransformation) + { + CustomTransformation.PokeString e = (CustomTransformation.PokeString) ((CustomTransformation) entry.transform).child(); + res.add(e.str); + } + } + return res; + }); + assertEquals(expected, actual); + }); + } + } + + @Test + public void testConcurrentCommit() throws Throwable + { + final int threadsPerNode = 10; + final int iterations = 100; + + try (Cluster cluster = builder().withNodes(3) + // test assumes that all nodes have all events, that is not true if snapshotting + .withConfig(config -> config.set("metadata_snapshot_frequency", Integer.MAX_VALUE) + .set("cms_await_timeout", "120000ms") + .set("cms_default_max_retries", 100)) + // todo; add config param to disable snapshotting + .start()) + { + Set expected = ConcurrentHashMap.newKeySet(); + List threads = new ArrayList<>(); + + CountDownLatch waitForStart = CountDownLatch.newCountDownLatch(1); + CountDownLatch waitForFinish = CountDownLatch.newCountDownLatch(threadsPerNode * cluster.size()); + + for (int j = 1; j <= cluster.size(); j++) + { + int node = j; + for (int i = 0; i < threadsPerNode; i++) + { + int thread = i; + threads.add(new Thread(() -> { + waitForStart.awaitUninterruptibly(); + try + { + for (int k = 0; k < iterations; k++) + { + String str = String.format("test payload %d %d %d", node, thread, k); + try + { + cluster.get(node).runOnInstance(() -> { + ClusterMetadataService.instance().commit(CustomTransformation.make(str)); + }); + expected.add(str); + } + catch (Throwable t) + { + // ignore + } + } + } + catch (Throwable t) + { + t.printStackTrace(); + throw t; + } + finally + { + waitForFinish.decrement(); + } + }, String.format("Writer#%d_%d", node, i))); + } + } + + for (Thread thread : threads) + thread.start(); + + waitForStart.decrement(); + waitForFinish.awaitUninterruptibly(); + + cluster.forEach(node -> { + Set actual = node.callOnInstance(() -> { + ClusterMetadataService.instance().processor().fetchLogAndWait(); + + Set res = new HashSet<>(); + + for (Entry entry : LogStorage.SystemKeyspace.getLogState(Epoch.FIRST).transformations.entries()) + { + if (entry.transform instanceof CustomTransformation) + { + CustomTransformation.PokeString e = (CustomTransformation.PokeString) ((CustomTransformation) entry.transform).child(); + res.add(e.str); + } + } + return res; + }); + HashSet diff = new HashSet<>(expected); + diff.removeAll(actual); + assertTrue(diff.toString(), diff.isEmpty()); + }); + } + } + // TODO: ensure exactly once delivery / callback? + // TODO: concurrent submission? +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java new file mode 100644 index 0000000000..a22eb09167 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java @@ -0,0 +1,174 @@ +/* + * 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.log; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; + +import org.junit.Assert; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.model.sut.SystemUnderTest; +import harry.visitors.GeneratingVisitor; +import harry.visitors.MutatingRowVisitor; +import harry.visitors.Visitor; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJVMTokenAwareVisitorExecutor; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.ClusterUtils.SerializableBiPredicate; +import org.apache.cassandra.locator.ReplicationFactor; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.transformations.CancelInProgressSequence; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.streaming.IncomingStream; +import org.apache.cassandra.streaming.StreamReceiveTask; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.shared.ClusterUtils.cancelInProgressSequences; +import static org.apache.cassandra.distributed.shared.ClusterUtils.decommission; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getClusterMetadataVersion; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; + +public class FailedLeaveTest extends FuzzTestBase +{ + private static int WRITES = 2000; + + @Test + public void resumeDecommissionWithStreamingFailureTest() throws Throwable + { + // After the leave operation fails (and we've re-enabled streaming), retry it + // and wait for a FINISH_LEAVE event to be successfully committed. + failedLeaveTest((ex, inst) -> ex.submit(() -> decommission(inst)), + (e, r) -> e instanceof PrepareLeave.FinishLeave && r.isSuccess()); + } + + @Test + public void cancelDecommissionWithStreamingFailureTest() throws Throwable + { + // After the leave operation fails, cancel it and wait for a CANCEL_SEQUENCE event + // to be successfully committed. + failedLeaveTest((ex, inst) -> ex.submit(() -> cancelInProgressSequences(inst)), + (e, r) -> e instanceof CancelInProgressSequence && r.isSuccess()); + } + + private void failedLeaveTest(BiFunction> runAfterFailure, + SerializableBiPredicate actionCommitted) + throws Exception + { + ExecutorService es = Executors.newSingleThreadExecutor(); + Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build()); + + + try (Cluster cluster = builder().withNodes(3) + .withInstanceInitializer(BB::install) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + IInvokableInstance leavingInstance = cluster.get(2); + configBuilder.setSUT(() -> new InJvmSut(cluster)); + Run run = configBuilder.build().createRun(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + + QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2)); + Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.ALL)); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + Epoch startEpoch = getClusterMetadataVersion(cmsInstance); + // Configure node 3 to fail when receiving streams, then start decommissioning node 2 + cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(true)); + Future success = es.submit(() -> decommission(leavingInstance)); + Assert.assertFalse(success.get()); + + // metadata event log should have advanced by 2 entries, PREPARE_LEAVE & START_LEAVE + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + Assert.assertEquals(startEpoch.getEpoch() + 2, currentEpoch.getEpoch()); + + // Node 2's leaving failed due to the streaming errors. If decommission is called again on the node, it should + // resume where it left off. Allow streaming to succeed this time and verify that the node is able to + // finish leaving. + cluster.get(3).runOnInstance(() -> BB.failReceivingStream.set(false)); + + // Run the desired action to mitigate the failure (i.e. retry or cancel) + success = runAfterFailure.apply(es, leavingInstance); + + // get the Epoch of the event resulting from that action, so we can wait for it + Epoch nextEpoch = getSequenceAfterCommit(cmsInstance, actionCommitted).call(); + + Assert.assertTrue(success.get()); + + // wait for the cluster to all witness the event submitted after failure + // (i.e. the FINISH_JOIN or CANCEL_SEQUENCE). + ClusterUtils.waitForCMSToQuiesce(cluster, nextEpoch); + + //validate the state of the cluster + for (int i = 0; i < WRITES; i++) + visitor.visit(); + model.validateAll(); + } + } + + + public static class BB + { + static AtomicBoolean failReceivingStream = new AtomicBoolean(false); + public static void install(ClassLoader cl, int instance) + { + if (instance == 3) + { + new ByteBuddy().rebase(StreamReceiveTask.class) + .method(named("received")) + .intercept(MethodDelegation.to(BB.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + } + + public static void received(IncomingStream stream, @SuperCall Callable zuper) throws Exception + { + if (failReceivingStream.get()) + throw new RuntimeException("XXX Stream receiving error"); + zuper.call(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FetchLogFromPeersTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/FetchLogFromPeersTest.java new file mode 100644 index 0000000000..d0ad1f2f34 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FetchLogFromPeersTest.java @@ -0,0 +1,367 @@ +/* + * 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.log; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.IMessageFilters; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.impl.Instance; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.transformations.SealPeriod; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class FetchLogFromPeersTest extends TestBaseImpl +{ + enum ClusterState { COORDINATOR_BEHIND, REPLICA_BEHIND } + enum Operation { READ, WRITE } + + @Test + public void testSchema() throws Exception + { + try (Cluster cluster = init(builder().withNodes(3) + .start())) + { + cluster.schemaChange(withKeyspace("alter keyspace %s with replication = {'class':'SimpleStrategy', 'replication_factor':3}")); + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key)")); + + for (ClusterState clusterState : ClusterState.values()) + for (Operation operation : Operation.values()) + { + setupSchemaBehind(cluster); + runQuery(cluster, clusterState, operation); + } + } + } + + public int coordinator(ClusterState clusterState) + { + switch (clusterState) + { + case COORDINATOR_BEHIND: + return 2; + case REPLICA_BEHIND: + return 3; + } + throw new IllegalStateException(); + } + + public void runQuery(Cluster cluster, ClusterState clusterState, Operation operation) throws ExecutionException, InterruptedException + { + cluster.get(1).shutdown().get(); + + // node2 is behind + String query; + switch (operation) + { + case READ: + query = "select * from %s.tbl where id = 5"; + break; + case WRITE: + query = "insert into %s.tbl (id) values (5)"; + break; + default: + throw new IllegalStateException(); + } + int coordinator = coordinator(clusterState); + long mark = cluster.get(2).logs().mark(); + long metricsBefore = cluster.get(2).callOnInstance(() -> TCMMetrics.instance.fetchedPeerLogEntries.getCount()); + if (clusterState == ClusterState.COORDINATOR_BEHIND) + { + long [] coordinatorBehindMetricsBefore = new long[cluster.size()]; + try + { + for (int i = 1; i <= cluster.size(); i++) + if (!cluster.get(i).isShutdown()) + coordinatorBehindMetricsBefore[i - 1] = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindSchema.getCount()); + cluster.coordinator(coordinator).execute(withKeyspace(query), ConsistencyLevel.QUORUM); + fail("should fail"); + } + catch (Exception ignored) {} + + boolean metricBumped = false; + for (int i = 1; i <= cluster.size(); i++) + { + if (i == coordinator || cluster.get(i).isShutdown()) + continue; + long metricAfter = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindSchema.getCount()); + if (metricAfter - coordinatorBehindMetricsBefore[i - 1] > 0) + { + metricBumped = true; + break; + } + } + assertTrue("Metric CoordinatorBehindSchema should have been bumped for at least one replica", metricBumped); + + } + cluster.coordinator(coordinator).execute(withKeyspace(query), ConsistencyLevel.QUORUM); + assertTrue(cluster.get(2).logs().grep(mark, "Fetching log from /127.0.0.3:7012").getResult().size() > 0); + long metricsAfter = cluster.get(2).callOnInstance(() -> TCMMetrics.instance.fetchedPeerLogEntries.getCount()); + assertTrue(metricsAfter > metricsBefore); + + cluster.get(1).startup(); + } + + public void setupSchemaBehind(Cluster cluster) + { + cluster.filters().reset(); + cluster.filters().inbound().from(1).to(2).drop(); + long epochBefore = cluster.get(3).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + cluster.coordinator(1).execute(withKeyspace("alter table %s.tbl with comment='test " + UUID.randomUUID() + "'"), ConsistencyLevel.ONE); + cluster.get(3).runOnInstance(() -> { + try + { + ClusterMetadataService.instance().awaitAtLeast(Epoch.create(epochBefore).nextEpoch()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + }); + cluster.filters().reset(); + } + + @Test + public void catchupCoordinatorBehindTestPlacements() throws Exception + { + try (Cluster cluster = init(builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .start())) + { + cluster.schemaChange(withKeyspace("alter keyspace %s with replication = {'class':'SimpleStrategy', 'replication_factor':3}")); + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + + cluster.filters().inbound().from(1).to(2).drop(); + + IInstanceConfig config = cluster.newInstanceConfig(); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(cluster); + + cluster.get(1).shutdown().get(); + + // node2 is behind, reading from it will cause a failure, but it will then catch up + try + { + cluster.coordinator(2).execute(withKeyspace("insert into %s.tbl (id) values (3)"), ConsistencyLevel.QUORUM); + fail("writing should fail"); + } + catch (Exception ignored) {} + + cluster.coordinator(2).execute(withKeyspace("insert into %s.tbl (id) values (3)"), ConsistencyLevel.QUORUM); + } + } + + @Test + public void catchupCoordinatorAheadPlacementsReadTest() throws Exception + { + try (Cluster cluster = init(builder().withNodes(4) + .start())) + { + cluster.schemaChange(withKeyspace("alter keyspace %s with replication = {'class':'SimpleStrategy', 'replication_factor':3}")); + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + cluster.filters().inbound().from(1).to(2).drop(); + AtomicInteger fetchedFromPeer = new AtomicInteger(); + cluster.filters().inbound().from(2).to(4).messagesMatching((from, to, msg) -> { + if (msg.verb() == Verb.TCM_FETCH_PEER_LOG_REQ.id) + fetchedFromPeer.getAndIncrement(); + return false; + }).drop().on(); + cluster.get(3).shutdown().get(); + cluster.get(4).nodetoolResult("assassinate", "127.0.0.3").asserts().success(); + + cluster.get(1).shutdown().get(); + int before = fetchedFromPeer.get(); + cluster.coordinator(4).execute(withKeyspace("select * from %s.tbl where id = 6"), ConsistencyLevel.QUORUM); + assertTrue(fetchedFromPeer.get() > before); + } + } + + @Test + public void catchupCoordinatorAheadPlacementsWriteTest() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(4) + .start())) + { + cluster.schemaChange(withKeyspace("alter keyspace %s with replication = {'class':'SimpleStrategy', 'replication_factor':3}")); + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key)")); + cluster.filters().inbound().from(1).to(2).drop(); + AtomicInteger fetchedFromPeer = new AtomicInteger(); + cluster.filters().inbound().from(2).to(4).messagesMatching((from, to, msg) -> { + if (msg.verb() == Verb.TCM_FETCH_PEER_LOG_REQ.id) + fetchedFromPeer.getAndIncrement(); + return false; + }).drop().on(); + + cluster.get(3).shutdown().get(); + cluster.get(4).nodetoolResult("assassinate", "127.0.0.3").asserts().success(); + + cluster.get(1).shutdown().get(); + // node4 is ahead - node2 should catch up and allow the write + int before = fetchedFromPeer.get(); + long mark = cluster.get(2).logs().mark(); + cluster.coordinator(4).execute(withKeyspace("insert into %s.tbl (id) values (6)"), ConsistencyLevel.QUORUM); + assertTrue(cluster.get(2).logs().grep(mark, "Routing is correct, but coordinator needs to catch-up to maintain consistency.").getResult().isEmpty()); + assertTrue(fetchedFromPeer.get() > before); + + // Should succeed after blocking catch-up. + cluster.coordinator(4).execute(withKeyspace("insert into %s.tbl (id) values (6)"), ConsistencyLevel.QUORUM); + } + } + + @Test + public void testCMSCatchupTest() throws Exception + { + try (Cluster cluster = init(builder().withNodes(4) + .withConfig(c -> c.with(Feature.NETWORK, Feature.GOSSIP)) // needed for addtocms below + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + cluster.get(2).runOnInstance(() -> AddToCMS.initiate()); + cluster.get(3).runOnInstance(() -> AddToCMS.initiate()); + // isolate node2 from the other CMS members to ensure it's behind + cluster.filters().inbound().from(1).to(2).drop(); + cluster.filters().inbound().from(3).to(2).drop(); + AtomicInteger fetchedFromPeer = new AtomicInteger(); + cluster.filters().inbound().from(2).to(4).messagesMatching((from, to, msg) -> { + if (msg.verb() == Verb.TCM_FETCH_PEER_LOG_REQ.id) + fetchedFromPeer.getAndIncrement(); + return false; + }).drop().on(); + + long mark = cluster.get(4).logs().mark(); + cluster.coordinator(1).execute(withKeyspace("alter table %s.tbl with comment='test 123'"), ConsistencyLevel.ONE); + cluster.get(4).logs().watchFor(mark, "AlterOptions"); + + mark = cluster.get(2).logs().mark(); + cluster.get(1).shutdown().get(); + cluster.get(2).logs().watchFor(mark, "/127.0.0.1:7012 state jump to shutdown"); + // node2, a CMS member, is now behind and node1 is shut down. + // Try reading at QUORUM from node4, node2 should detect it's behind and catch up from node4 + int before = fetchedFromPeer.get(); + cluster.coordinator(4).execute(withKeyspace("select * from %s.tbl where id = 55"), ConsistencyLevel.QUORUM); + assertTrue(fetchedFromPeer.get() > before); + } + } + + @Test + public void catchupWithSnapshot() throws Exception + { + try (Cluster cluster = init(builder().withNodes(4) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + executeAlters(cluster); + + cluster.filters().inbound().to(2).messagesMatching((from, to, message) -> + cluster.get(2).callOnInstance(() -> { + Message decoded = Instance.deserializeMessage(message); + if (decoded.payload instanceof Replication) + { + Replication rep = (Replication) decoded.payload; + // drop every other replication message to make sure pending buffer is non-consecutive + if (decoded.epoch().getEpoch() % 2 == 0 && + rep.entries().stream().noneMatch((e) -> e.transform instanceof SealPeriod)) + return false; + } + return true; + }) + ).drop(); + executeAlters(cluster); + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + executeAlters(cluster); + cluster.filters().reset(); + + long epoch = cluster.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().fetchLogFromPeerAsync(InetAddressAndPort.getByNameUnchecked("127.0.0.1"), Epoch.create(epoch))); + + long epochNode2 = 0; + while (epochNode2 < epoch) + { + epochNode2 = cluster.get(2).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + if (epochNode2 < epoch) + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); + } + } + } + + @Test + public void skipEntryAndCatchupWithSnapshot() throws Exception + { + try (Cluster cluster = init(builder().withNodes(2) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key)")); + + IMessageFilters.Filter filter = cluster.filters().inbound().to(2).drop(); + cluster.coordinator(1).execute(withKeyspace("alter table %s.tbl with comment='"+UUID.randomUUID()+"'"), ConsistencyLevel.QUORUM); + filter.off(); + + filter = cluster.filters().inbound().to(2).verbs(Verb.TCM_FETCH_PEER_LOG_RSP.id, Verb.TCM_FETCH_CMS_LOG_REQ.id).drop(); + executeAlters(cluster); + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + executeAlters(cluster); + filter.off(); + + try + { + // Trigger a single read to make sure fetch or CMS logs can be fetched + cluster.coordinator(2).execute(withKeyspace("select * from %s.tbl"), ConsistencyLevel.ALL); + } + catch (Throwable t) + { + // ignore coordinator behind + } + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + } + } + + + private static void executeAlters(Cluster cluster) + { + for (int i = 0; i < 10; i++) + cluster.coordinator(1).execute(withKeyspace("alter table %s.tbl with comment='"+UUID.randomUUID()+"'"), ConsistencyLevel.QUORUM); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ForceSnapshotTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ForceSnapshotTest.java new file mode 100644 index 0000000000..476638e321 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ForceSnapshotTest.java @@ -0,0 +1,157 @@ +/* + * 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.log; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.Test; + +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + + +public class ForceSnapshotTest extends TestBaseImpl +{ + @Test + public void testForceSnapshot() throws IOException + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(c -> c.set("unsafe_tcm_mode", "true")) + .start())) + { + cluster.get(2).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + + Directory d = metadata.directory.with(new NodeAddresses(UUID.randomUUID(), + InetAddressAndPort.getByNameUnchecked("127.0.0.5"), + InetAddressAndPort.getByNameUnchecked("127.0.0.5"), + InetAddressAndPort.getByNameUnchecked("127.0.0.5")), + new Location("AA", "BB")); + metadata = metadata.transformer().with(d).build().metadata; + + ClusterMetadataService.instance().forceSnapshot(metadata); + }); + + cluster.forEach(() -> assertEquals(InetAddressAndPort.getByNameUnchecked("127.0.0.5"), ClusterMetadata.current().directory.endpoint(new NodeId(4)))); + } + } + + @Test + public void testRevertToEpoch() throws IOException + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(c -> c.set("metadata_snapshot_frequency", 7) + .set("unsafe_tcm_mode", "true")) + .start())) + { + Map tblToEpoch = new HashMap<>(); + for (int i = 0; i < 20; i++) + { + // we create table "x"+i at this epoch - make sure the table is gone when revert to it + tblToEpoch.put(i, cluster.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch())); + cluster.schemaChange(withKeyspace("create table %s.x" + i + " (id int primary key)")); + } + int startReverting = 15; + for (int i = 0; i < 10; i++) + { + long revertTo = tblToEpoch.get(startReverting - i); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().revertToEpoch(Epoch.create(revertTo))); + for (int j = 0; j < startReverting - i; j++) + cluster.coordinator(1).execute(withKeyspace("insert into %s.x" + j + " (id) values (1)"), ConsistencyLevel.ALL); + try + { + int goneTable = startReverting - i; + cluster.coordinator(1).execute(withKeyspace("insert into %s.x " + goneTable + " (id) values (1)"), ConsistencyLevel.ALL); + fail("Table x" + goneTable + " should not exist"); + } + catch (Exception e) + { + //ignore + } + } + } + } + + @Test + public void testDumpLoadMetadata() throws IOException + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(c -> c.set("metadata_snapshot_frequency", 7) + .set("unsafe_tcm_mode", "true")) + .start())) + { + String filename = null; + for (int i = 0; i < 20; i++) + { + if (i == 10) + { + filename = cluster.get(2).callOnInstance(() -> { + try + { + return ClusterMetadataService.instance().dumpClusterMetadata(Epoch.EMPTY, ClusterMetadata.current().epoch, NodeVersion.CURRENT_METADATA_VERSION); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + } + cluster.schemaChange(withKeyspace("create table %s.x" + i + " (id int primary key)")); + } + assertNotNull(filename); + for (int i = 0; i < 20; i++) + cluster.coordinator(1).execute(withKeyspace("insert into %s.x"+i+" (id) values (1)"), ConsistencyLevel.ALL); + String loadFilename = filename; + cluster.forEach(() -> assertEquals(20, Keyspace.open(KEYSPACE).getColumnFamilyStores().size())); + cluster.get(1).runOnInstance(() -> { + try + { + ClusterMetadataService.instance().loadClusterMetadata(loadFilename); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + cluster.forEach(() -> assertEquals(10, Keyspace.open(KEYSPACE).getColumnFamilyStores().size())); + + // make sure we execute more transformations; + cluster.schemaChange(withKeyspace("create table %s.yyy (id int primary key)")); + + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java new file mode 100644 index 0000000000..02f6b3eb91 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java @@ -0,0 +1,142 @@ +/* + * 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.log; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.BeforeClass; + +import harry.core.Run; +import harry.data.ResultSetRow; +import harry.model.OpSelectors; +import harry.model.QuiescentChecker; +import harry.model.sut.SystemUnderTest; +import harry.operations.CompiledStatement; +import harry.operations.Query; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.test.ExecUtil; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.locator.ReplicationFactor; + +import static harry.model.SelectHelper.resultSetToRow; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +public class FuzzTestBase extends TestBaseImpl +{ + @BeforeClass + public static void beforeClass() throws Throwable + { + TestBaseImpl.beforeClass(); + HarryHelper.init(); + } + + @Override + public Cluster.Builder builder() { + return super.builder() + .withConfig(cfg -> cfg.with(GOSSIP, NETWORK) + // Since we'll be pausing the commit request, it may happen that it won't get + // unpaused before event expiration. + .set("request_timeout", String.format("%dms", TimeUnit.MINUTES.toMillis(10)))); + } + + public static IIsolatedExecutor.SerializableRunnable toRunnable(ExecUtil.ThrowingSerializableRunnable runnable) + { + return () -> { + try + { + runnable.run(); + } + catch (Throwable t) + { + System.out.println(t.getMessage()); + t.printStackTrace(); + } + }; + } + + public static class QuiescentLocalStateChecker extends QuiescentChecker + { + public final InJvmSut inJvmSut; + public final ReplicationFactor rf; + private final OpSelectors.PdSelector pdSelector; + + public QuiescentLocalStateChecker(Run run) + { + this(run, null); + } + + public QuiescentLocalStateChecker(Run run, ReplicationFactor rf) + { + super(run); + assert run.sut instanceof InJvmSut; + + this.inJvmSut = (InJvmSut) run.sut; + this.rf = rf; + this.pdSelector = run.pdSelector; + } + + public void validateAll() + { + for (int lts = 0; lts < clock.peek(); lts++) + validate(Query.selectPartition(schema, pdSelector.pd(lts, schema), false)); + } + + @Override + public void validate(Query query) + { + CompiledStatement compiled = query.toSelectStatement(); + int[] replicas = inJvmSut.getReadReplicasFor(schema.inflatePartitionKey(query.pd), schema.keyspace, schema.table); + if (rf != null && replicas.length != rf.allReplicas) + throw new IllegalStateException(String.format("Total number of replicas %d does not match expectation %d", + replicas.length, rf.allReplicas)); + + for (int node : replicas) + { + try + { + validate(() -> { + Object[][] objects = inJvmSut.execute(compiled.cql(), + SystemUnderTest.ConsistencyLevel.NODE_LOCAL, + node, + compiled.bindings()); + List result = new ArrayList<>(); + for (Object[] obj : objects) + result.add(resultSetToRow(query.schemaSpec, clock, obj)); + + return result; + }, query); + } + catch (ValidationException e) + { + throw new AssertionError(String.format("Caught error while validating replica %d of replica set %s", + node, Arrays.toString(replicas)), + e); + } + } + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java new file mode 100644 index 0000000000..720d57f919 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java @@ -0,0 +1,147 @@ +/* + * 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.log; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import com.google.monitoring.runtime.instrumentation.common.util.concurrent.Uninterruptibles; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.GossipDigest; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.utils.concurrent.Future; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.junit.Assert.assertEquals; + +public class GossipDeadlockTest extends TestBaseImpl +{ + @Test + public void testPendingDeadlock() throws IOException, ExecutionException, InterruptedException + { + + try (Cluster cluster = init(builder().withNodes(4) + .withConfig(config -> config.with(NETWORK, GOSSIP) + .set("progress_barrier_default_consistency_level", "NODE_LOCAL") + .set("progress_barrier_min_consistency_level", "NODE_LOCAL")) + .withoutVNodes() + .withInstanceInitializer(BB::install) // make GossipTask.run slower to increase the deadlock window + .start())) + { + cluster.schemaChange(withKeyspace("alter keyspace %s with replication = {'class': 'SimpleStrategy', 'replication_factor':1 }")); + cluster.schemaChange("alter keyspace system_distributed with replication = {'class': 'SimpleStrategy', 'replication_factor':1 }"); + cluster.schemaChange("alter keyspace system_traces with replication = {'class': 'SimpleStrategy', 'replication_factor':1 }"); + + ExecutorPlus e = ExecutorFactory.Global.executorFactory().pooled("BounceMove", 2); + long startToken = cluster.get(2).callOnInstance(() -> { + NodeId nodeId = ClusterMetadata.current().myNodeId(); + return ((Murmur3Partitioner.LongToken)ClusterMetadata.current().tokenMap.tokens(nodeId).get(0)).token; + }); + AtomicBoolean stop = new AtomicBoolean(false); + Future moves = e.submit(() -> { + long token = startToken; + while (!stop.get()) + { + token++; + cluster.get(2).nodetoolResult("move", String.valueOf(token)).asserts().success(); + } + return (int)(token - startToken); + }); + Future bounces = e.submit(() -> { + int iters = 0; + while (!stop.get()) + { + cluster.get(4).nodetoolResult("disablegossip").asserts().success(); + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + cluster.get(4).nodetoolResult("enablegossip").asserts().success(); + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + iters++; + } + return iters; + }); + + Uninterruptibles.sleepUninterruptibly(120, TimeUnit.SECONDS); + stop.set(true); + System.out.println("MOVES = " + moves.get()); + System.out.println("BOUNCES = " + bounces.get()); + + long epoch = cluster.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + for (int i = 1; i <= 4; i++) + assertEquals(epoch, (long)cluster.get(i).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch())); + } + } + + public static class BB + { + public static void install(ClassLoader cl, int i) + { + new ByteBuddy().rebase(Gossiper.class) + .method(named("makeRandomGossipDigest")).intercept(MethodDelegation.to(BB.class)) + .method(named("unsafeUpdateEpStates")).intercept(MethodDelegation.to(BB.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + + public static void makeRandomGossipDigest(List digests, @SuperCall Callable zuper) + { + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + try + { + zuper.call(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static void unsafeUpdateEpStates(InetAddressAndPort endpoint, EndpointState epstate, @SuperCall Callable zuper) + { + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + try + { + zuper.call(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/InProgressSequenceCoordinationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/InProgressSequenceCoordinationTest.java new file mode 100644 index 0000000000..d3930df032 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/InProgressSequenceCoordinationTest.java @@ -0,0 +1,406 @@ +/* + * 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.log; + +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.function.BiFunction; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.IMessage; +import org.apache.cassandra.distributed.api.IMessageFilters; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.SequenceState; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.utils.concurrent.Condition; + +import static org.apache.cassandra.config.CassandraRelevantProperties.REPLACE_ADDRESS_FIRST_BOOT; +import static org.apache.cassandra.distributed.Constants.KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN; +import static org.apache.cassandra.distributed.Constants.KEY_DTEST_FULL_STARTUP; +import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.net.Verb.TCM_CURRENT_EPOCH_REQ; +import static org.apache.cassandra.net.Verb.TCM_FETCH_PEER_LOG_RSP; +import static org.apache.cassandra.tcm.sequences.SequenceState.blocked; +import static org.apache.cassandra.tcm.sequences.SequenceState.continuable; +import static org.apache.cassandra.tcm.sequences.SequenceState.halted; + +public class InProgressSequenceCoordinationTest extends FuzzTestBase +{ + @Test + public void bootstrapProgressTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .appendConfig(cfg -> cfg.set("progress_barrier_timeout", "5000ms") + .set("request_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms")) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .start()) + { + + IInvokableInstance cmsInstance = cluster.get(1); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(KEY_DTEST_FULL_STARTUP, false) + .set(KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false); + IInvokableInstance newInstance = cluster.bootstrap(config); + + // Set expectation of finish join & retrieve the epoch when it eventually gets committed + Callable finishJoinEpoch = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + + // Drop all messages from the CMS and LOG_REPLICATION from the joining node going to nodes 2 & 3. This will + // ensure that they do not receive the join events for the new instance. They will then not be able to ack + // them as the joining node attempts to progress its startup sequence, which should consequently fail. + cluster.filters().allVerbs().from(1).to(2,3).drop(); + cluster.filters().verbs(TCM_FETCH_PEER_LOG_RSP.id).from(4).to(2, 3).drop(); + + // Have the joining node pause when the mid join event fails due to ack timeout. + // The StartJoin event has no prerequisite, so we should see a CONTINUING state, but execution of the + // MidJoin should be BLOCKED as nodes 2 & 3 can't ack the StartJoin. + Callable progressBlocked = waitForListener(newInstance, continuable(), blocked()); + new Thread(() -> newInstance.startup()).start(); + progressBlocked.call(); + + // Remove the partition between nodes 2 & 3 and the CMS and have them catch up. + cluster.filters().reset(); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + cluster.get(3).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + + // Unpause the joining node and have it retry the bootstrap sequence from MidJoin, this time the + // expectation is that it will succeed, so we can just clear out the listener. + newInstance.runOnInstance(() -> { + TestExecutionListener listener = (TestExecutionListener) InProgressSequences.listener; + listener.restorePrevious(); + listener.releaseAndRetry(); + }); + + // Wait for the cluster to all witness the finish join event. + Epoch finalEpoch = finishJoinEpoch.call(); + ClusterUtils.waitForCMSToQuiesce(cluster, finalEpoch); + } + } + + @Test + public void decommissionProgressTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(4) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .appendConfig(cfg -> cfg.set("progress_barrier_timeout", "5000ms") + .set("request_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms")) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + + // Set expectation of finish leave & retrieve the epoch when it eventually gets committed + Callable finishLeaveEpoch = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareLeave.FinishLeave && r.isSuccess()); + + // Drop all messages from the CMS and LOG_REPLICATION from the leaving node going to nodes 2 & 3. This will + // ensure that they do not receive the leave events for the new instance. They will then not be able to ack + // them as the leaving node attempts to progress its leave sequence, which should consequently fail. + cluster.filters().allVerbs().from(1).to(2,3).drop(); + cluster.filters().verbs(TCM_FETCH_PEER_LOG_RSP.id).from(4).to(2, 3).drop(); + + // Have the joining node pause when the StartLeave event fails due to ack timeout. + IInvokableInstance leavingInstance = cluster.get(4); + Callable progressBlocked = waitForListener(leavingInstance, blocked()); + Thread t = new Thread(() -> leavingInstance.runOnInstance(() -> StorageService.instance.decommission(true))); + t.start(); + progressBlocked.call(); + // Remove the partition between nodes 2 & 3 and the CMS and have them catch up. + cluster.filters().reset(); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + cluster.get(3).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + + // Now re-partition nodes 2 & 3 so that the MidLeave event cannot be submitted by node 1 as 2 & 3 won't + // receive/ack the StartLeave. + cluster.filters().allVerbs().from(1).to(2,3).drop(); + cluster.filters().verbs(TCM_FETCH_PEER_LOG_RSP.id).from(4).to(2, 3).drop(); + + // Unpause the leaving node and have it retry the StartLeave, which should now be able to proceed as 2 & 3 + // will ack the PrepareJoin. Its progress should be blocked as it comes to submit the next event, its + // MidJoin, so set a new BLOCKED expectation. + progressBlocked = waitForExistingListener(leavingInstance, blocked()); + leavingInstance.runOnInstance(() -> { + TestExecutionListener listener = (TestExecutionListener) InProgressSequences.listener; + listener.releaseAndRetry(); + }); + progressBlocked.call(); + + // Heal the partition again and force a catch up. + cluster.filters().reset(); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + cluster.get(3).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + + // Unpause the leaving node and have it retry the MidLeave, which will now be able to proceed as 2 & 3 will + // ack the StartJoin. This time the expectation is that the remaining events will be successfully acked, so + // we can just clear out the listener. + leavingInstance.runOnInstance(() -> { + TestExecutionListener listener = (TestExecutionListener) InProgressSequences.listener; + listener.restorePrevious(); + listener.releaseAndRetry(); + }); + + // Wait for the cluster to all witness the finish join event. + Epoch finalEpoch = finishLeaveEpoch.call(); + ClusterUtils.waitForCMSToQuiesce(cluster, finalEpoch); + } + } + + @Test + public void replacementProgressTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .appendConfig(cfg -> cfg.set("progress_barrier_timeout", "5000ms") + .set("request_timeout", "1000ms") + .set("progress_barrier_backoff", "100ms")) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .start()) + { + + IInvokableInstance cmsInstance = cluster.get(1); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance); + + IInvokableInstance toReplace = cluster.get(3); + toReplace.shutdown(false); + IInvokableInstance replacement = addInstance(cluster, + toReplace.config(), + c -> c.set("auto_bootstrap", true) + .set(KEY_DTEST_FULL_STARTUP, false) + .set(KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false)); + + // Set expectation of FinishReplace & retrieve the epoch when it eventually gets committed + Callable finishReplaceEpoch = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareReplace.FinishReplace && r.isSuccess()); + + // Drop all messages from the progress barrier discovery from the joining node going. This will + // ensure that it does not receive the replace/join events for the new instance. It will then not be able to + // ack them as the new node attempts to progress its startup sequence, which should consequently fail. + cluster.filters().verbs(TCM_CURRENT_EPOCH_REQ.id).from(4).drop(); + + // Have the joining node pause when the StartReplace event fails due to ack timeout. + Callable progressBlocked = waitForListener(replacement, continuable(), blocked()); + new Thread(() -> { + try (WithProperties replacementProps = new WithProperties()) + { + replacementProps.set(REPLACE_ADDRESS_FIRST_BOOT, + toReplace.config().broadcastAddress().getAddress().getHostAddress()); + replacement.startup(); + } + }).start(); + progressBlocked.call(); + + // Remove the partition between node 2 and the CMS and have it catch up. + cluster.filters().reset(); + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + + // Unpause the joining node and have it retry the bootstrap sequence from StartReplace, this time the + // expectation is that it will succeed, so we can just clear out the listener. + replacement.runOnInstance(() -> { + TestExecutionListener listener = (TestExecutionListener) InProgressSequences.listener; + listener.restorePrevious(); + listener.releaseAndRetry(); + }); + + // Wait for the cluster to all witness the finish join event. + Epoch finalEpoch = finishReplaceEpoch.call(); + ClusterUtils.waitForCMSToQuiesce(cluster, finalEpoch); + } + } + + @Test + public void rejectSubsequentInProgressSequence() throws Throwable + { + try (Cluster cluster = builder().withNodes(2) + .start()) + { + cluster.get(2).runOnInstance(() -> { + NodeId self = ClusterMetadata.current().myNodeId(); + ClusterMetadataService.instance().commit(new PrepareLeave(self, + true, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + try + { + AddToCMS.initiate(); + Assert.fail("Should have failed"); + } + catch (Throwable t) + { + Assert.assertTrue(t.getMessage().contains("since it already has an active in-progress sequence")); + } + }); + } + } + + @Test + public void inProgressSequenceRetryTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(2)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(2, "dc0", "rack0")) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP).set("request_timeout_in_ms", "1000")) + .start()) + { + cluster.filters() + .inbound() + .messagesMatching(new IMessageFilters.Matcher() + { + final Random rng = new Random(1); + public boolean matches(int i, int i1, IMessage msg) + { + if (msg.verb() != Verb.TCM_COMMIT_REQ.id) + return false; + return rng.nextBoolean(); + } + }).drop().on(); + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(); + } + } + + /** + * Ensure that sequential execution results into given SequenceStates. Provide a state for each + * expected operation. + */ + private Callable waitForListener(IInvokableInstance instance, SequenceState...expected) + { + Callable remoteCallable = instance.callOnInstance(() -> { + TestExecutionListener listener = new TestExecutionListener(expected); + listener.replaceCurrent(); + return () -> { + listener.await(); + return null; + }; + }); + return remoteCallable::call; + } + + private Callable waitForExistingListener(IInvokableInstance instance, SequenceState...expected) + { + Callable remoteCallable = instance.callOnInstance(() -> { + TestExecutionListener listener = (TestExecutionListener) InProgressSequences.listener; + listener.withNewExpectations(expected); + return () -> { + listener.await(); + return null; + }; + }); + return remoteCallable::call; + } + + public static class TestExecutionListener implements BiFunction, SequenceState, SequenceState> + { + volatile boolean retry = true; + volatile Condition expectationsMet = Condition.newOneTimeCondition(); + volatile Condition barrier; + volatile BiFunction, SequenceState, SequenceState> previous; + + SequenceState[] expectations; + int index = 0; + + TestExecutionListener(SequenceState... resumptionStates) + { + expectations = resumptionStates; + } + + public void replaceCurrent() + { + previous = InProgressSequences.replaceListener(this); + } + + public void restorePrevious() + { + InProgressSequences.replaceListener(previous); + } + + @Override + public SequenceState apply(MultiStepOperation sequence, SequenceState state) + { + if (null == expectations || expectations.length == 0) + return state; + + if (!state.equals(expectations[index])) + throw new IllegalStateException(String.format("Unexpected outcome for %s step %s; Expected: %s, Actual: %s", + sequence.kind(), sequence.idx, expectations[index], state)); + + if (++index == expectations.length) + { + barrier = Condition.newOneTimeCondition(); + expectationsMet.signal(); + barrier.awaitUninterruptibly(); + if (retry) + state = sequence.executeNext().isContinuable() + ? continuable() + : halted(); + return state; + } + + return state; + } + + public void await() + { + expectationsMet.awaitUninterruptibly(); + } + + public void withNewExpectations(SequenceState...newExpectations) + { + expectations = newExpectations; + index = 0; + expectationsMet = Condition.newOneTimeCondition(); + } + + public void releaseAndRetry() + { + retry = true; + barrier.signal(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java new file mode 100644 index 0000000000..857db9d8ba --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java @@ -0,0 +1,763 @@ +/* + * 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.log; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.CMSPlacementStrategy; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.SealPeriod; + +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.nodeFactoryHumanReadable; + +public class MetadataChangeSimulationTest extends CMSTestBase +{ + static + { + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + } + + private static final Random rng = new Random(1); + + @Test + public void simulateNTS() throws Throwable + { + // TODO: right now, we pick a candidate only if there is enough rf to execute operation + // but the problem is that if we start multiple operations that would take us under rf, we will screw up the placements + // this was not happening before, and test is crafted now to disallow such states, but this is a bug. + // we should either forbid this, or allow it, but make it work. + for (int concurrency : new int[]{ 1, 3, 5 }) + { + for (int rf : new int[]{ 2, 3, 5 }) + { + simulate(50, new PlacementSimulator.NtsReplicationFactor(3, rf), concurrency); + } + } + } + + @Test + public void simulateSimple() throws Throwable + { + for (int concurrency : new int[]{ 1, 3, 5 }) + { + for (int rf : new int[]{ 2, 3, 5 }) + { + simulate(50, new PlacementSimulator.SimpleReplicationFactor(rf), concurrency); + } + } + } + + @Test + public void testMoveReal() throws Throwable + { + for (int i = 0; i < 4; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 4); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 4); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 4); + } + + for (int i = 0; i < 5; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 5); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 5); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 650, 5); + } + + for (int i = 0; i < 10; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 10); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 10); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 1050, 10); + } + + for (int i = 0; i < 9; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 9); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 9); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 9); + } + + for (int i = 0; i < 10; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 10); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 10); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 10); + } + + for (int i = 0; i < 18; i++) + { + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 18); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 18); + testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 2050, 18); + } + + } + + public void testMoveReal(PlacementSimulator.ReplicationFactor rf, int moveNodeId, long moveToken, int numberOfNodes) throws Throwable + { + try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + ModelState state = ModelState.empty(nodeFactoryHumanReadable(), 10, 1); + Node movingNode = null; + for (int i = 0; i < numberOfNodes; i++) + { + int dc = (i % rf.dcs()) + 1; + ModelChecker.Pair registration = registerNewNode(state, sut, dc , 1); + if (moveNodeId == i) + movingNode = registration.r; + state = SimulatedOperation.joinWithoutBootstrap(registration.l, sut, registration.r); + } + + Node movingTo = movingNode.overrideToken(moveToken); + state = SimulatedOperation.move(sut, state, movingNode, movingTo); + + while (!state.inFlightOperations.isEmpty()) + { + state = state.inFlightOperations.get(0).advance(state); + validatePlacements(sut, state); + } + } + } + + @Test + public void testLeaveReal() throws Throwable + { + testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 1); + testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 5); + testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1); + testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5); + + } + + public void testLeaveReal(PlacementSimulator.ReplicationFactor rf, int decomNodeId) throws Throwable + { + try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + ModelState state = ModelState.empty(nodeFactoryHumanReadable(), 10, 1); + + Node decomNode = null; + for (int i = 0; i < 12; i++) + { + int dc = (i % rf.dcs()) + 1; + ModelChecker.Pair registration = registerNewNode(state, sut, dc, 1); + if (decomNodeId == i) + decomNode = registration.r; + state = SimulatedOperation.joinWithoutBootstrap(registration.l, sut, registration.r); + } + + state = SimulatedOperation.leave(sut, state, decomNode); + + while (!state.inFlightOperations.isEmpty()) + { + state = state.inFlightOperations.get(0).advance(state); + validatePlacements(sut, state); + } + } + } + + @Test + public void testJoinReal() throws Throwable + { + testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1); + testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5); + } + + public void testJoinReal(PlacementSimulator.ReplicationFactor rf, int joinNodeId) throws Throwable + { + try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + ModelState state = ModelState.empty(nodeFactoryHumanReadable(), 10, 1); + + Node joiningNode = null; + for (int i = 1; i <= 12; i++) + { + int dc = (i % rf.dcs()) + 1; + ModelChecker.Pair registration = registerNewNode(state, sut, dc, 1); + state = registration.l; + if (joinNodeId == i) + joiningNode = registration.r; + else + state = SimulatedOperation.joinWithoutBootstrap(registration.l, sut, registration.r); + } + + state = SimulatedOperation.join(sut, state, joiningNode); + + while (!state.inFlightOperations.isEmpty()) + { + state = state.inFlightOperations.get(0).advance(state); + validatePlacements(sut, state); + } + + } + } + + public void simulate(int toBootstrap, PlacementSimulator.ReplicationFactor rf, int concurrency) throws Throwable + { + System.out.printf("RUNNING SIMULATION. TO BOOTSTRAP: %s, RF: %s, CONCURRENCY: %s%n", + toBootstrap, rf, concurrency); + long startTime = System.currentTimeMillis(); + ModelChecker modelChecker = new ModelChecker<>(); + ClusterMetadataService.unsetInstance(); + modelChecker.init(ModelState.empty(PlacementSimulator.nodeFactory(), toBootstrap, concurrency), + new CMSSut(AtomicLongBackedProcessor::new, false, rf)) + // Sequentially bootstrap rf nodes first + .step((state, sut) -> state.currentNodes.isEmpty(), + (state, sut, entropySource) -> { + for (Map.Entry e : rf.asMap().entrySet()) + { + int dcRf = e.getValue(); + int dc = Integer.parseInt(e.getKey().replace("datacenter", "")); + + for (int i = 0; i < dcRf + 1; i++) + { + ModelChecker.Pair registration = registerNewNode(state, sut, dc, 1); + state = SimulatedOperation.joinWithoutBootstrap(registration.l, sut, registration.r); + } + } + return new ModelChecker.Pair<>(state, sut); + }) + // Plan the bootstrap of a new node + .step((state, sut) -> state.uniqueNodes >= rf.total() && state.shouldBootstrap(), + (state, sut, entropySource) -> { + int dc = rf.asMap().size() == 1 ? 1 : entropySource.nextInt(rf.asMap().size() - 1) + 1; + Node toAdd; + if (!state.registeredNodes.isEmpty()) + { + toAdd = state.registeredNodes.remove(0); + } + else + { + ModelChecker.Pair registration = registerNewNode(state, sut, dc, 1); + state = registration.l; + toAdd = registration.r; + } + + return new ModelChecker.Pair<>(SimulatedOperation.join(sut, state, toAdd), sut); + }) + // Plan the decommission of one of the previously bootstrapped nodes + .step((state, sut) -> state.shouldLeave(rf, rng), + (state, sut, entropySource) -> { + Node toRemove = getRemovalCandidate(state, entropySource); + return new ModelChecker.Pair<>(SimulatedOperation.leave(sut, state, toRemove), sut); + }) + // Plan the move of one of the previously bootstrapped nodes + .step((state, sut) -> state.shouldMove(rf, rng), + (state, sut, entropySource) -> { + Node toMove = getMoveCandidate(state, entropySource); + return new ModelChecker.Pair<>(SimulatedOperation.move(sut, state, toMove, toMove.withNewToken()), sut); + }) + // Plan the replacement of one of the previously bootstrapped nodes + .step((state, sut) -> state.shouldReplace(rf, rng), + (state, sut, entropySource) -> { + Node toReplace = getRemovalCandidate(state, entropySource); + ModelChecker.Pair registration = registerNewNode(state, sut, toReplace.tokenIdx(), toReplace.dcIdx(), toReplace.rackIdx()); + state = registration.l; + Node replacement = registration.r; + return new ModelChecker.Pair<>(SimulatedOperation.replace(sut, state, toReplace, replacement), sut); + }) + // If there are any planned or in-flight operations, pick one at random. Then, if the op can be + // cancelled, either cancel it completely or execute its next step. + .step((state, sut) -> state.shouldCancel(rng) && !state.inFlightOperations.isEmpty(), + (state, sut, entropySource) -> { + int idx = entropySource.nextInt(state.inFlightOperations.size()); + ModelState.Transformer transformer = state.transformer(); + SimulatedOperation oldOperationState = state.inFlightOperations.get(idx); + oldOperationState.cancel(sut, state.simulatedPlacements, transformer); + return pair(transformer.transform(), sut); + }) + // If not cancellable, just execute its next step. + .step((state, sut) -> !state.inFlightOperations.isEmpty(), + (state, sut, entropySource) -> { + int idx = entropySource.nextInt(state.inFlightOperations.size()); + SimulatedPlacements simulatedState = state.simulatedPlacements; + ModelState.Transformer transformer = state.transformer(); + + SimulatedOperation oldOperationState = state.inFlightOperations.get(idx); + oldOperationState.advance(simulatedState, transformer); + return pair(transformer.transform(), sut); + }) + .step((state, sut) -> rng.nextDouble() < 0.05, + (state, sut, entropySource) -> { + try + { + sut.service.commit(SealPeriod.instance); + } + catch (IllegalStateException e) + { + Assert.assertTrue(e.getMessage().contains("Have just sealed this period")); + } + return pair(state, sut); + }) + .invariant((state, sut) -> { + if (state.currentNodes.size() > 0) + validatePlacements(sut, state); + + return true; + }) + .exitCondition((state, sut) -> { + if (state.currentNodes.size() >= toBootstrap && state.inFlightOperations.size() == 0) + { + validatePlacementsFinal(sut, state); + sut.close(); + System.out.printf("(RF: %d, CONCURRENCY: %d, RUN TIME: %dms) - " + + "REGISTERED: %d, CURRENT SIZE: %d, REJECTED %d, INFLIGHT: %d" + + "FINISHED (join,replace,leave,move): %s" + + "CANCELLED (join,replace,leave,move): %s%n", + sut.rf.total(), concurrency, System.currentTimeMillis() - startTime, + state.uniqueNodes, state.currentNodes.size(), state.rejected, state.inFlightOperations.size(), + Arrays.toString(state.finished), + Arrays.toString(state.cancelled)); + return true; + } + + return false; + }) + .run(); + } + + @Test + public void simulateDCAwareBounces() throws Throwable + { + Random random = new Random(1L); + for (int i = 0; i < 10; i++) + { + PlacementSimulator.ReplicationFactor ntsRf = new PlacementSimulator.NtsReplicationFactor(3, 3); + Map cmsRf = new HashMap<>(); + for (String s : ntsRf.asMap().keySet()) + cmsRf.put(s, 3); + + simulateBounces(ntsRf, new CMSPlacementStrategy.DatacenterAware(cmsRf, (cm, n) -> random.nextInt(10) > 1), random); + } + } + + public void simulateBounces(PlacementSimulator.ReplicationFactor rf, CMSPlacementStrategy CMSConfigurationStrategy, Random random) throws Throwable + { + + try(CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + ModelState state = ModelState.empty(PlacementSimulator.nodeFactory(), 300, 1); + + for (Map.Entry e : rf.asMap().entrySet()) + { + int dc = Integer.parseInt(e.getKey().replace("datacenter", "")); + + for (int i = 0; i < 100; i++) + { + ModelChecker.Pair registration = registerNewNode(state, sut, dc, random.nextInt(5) + 1); + state = SimulatedOperation.joinWithoutBootstrap(registration.l, sut, registration.r); + } + } + + Set newCms = CMSConfigurationStrategy.reconfigure(sut.service.metadata().directory.toNodeIds(sut.service.metadata().fullCMSMembers()), + sut.service.metadata()); + + ClusterMetadata metadata = sut.service.metadata(); + + for (int i = 0; i < 100; i++) + { + Set bouncing = new HashSet<>(); + Set replicasFromBouncedReplicaSets = new HashSet<>(); + int j = 0; + outer: + for (VersionedEndpoints.ForRange placements : sut.service.metadata().placements.get(rf.asKeyspaceParams().replication).writes.replicaGroups().values()) + { + List replicas = new ArrayList<>(metadata.directory.toNodeIds(placements.get().endpoints())); + List bounceCandidates = new ArrayList<>(); + for (NodeId replica : replicas) + { + if (!replicasFromBouncedReplicaSets.contains(replica)) + bounceCandidates.add(replica); + else + continue outer; + } + + if (!bounceCandidates.isEmpty()) + { + NodeId toBounce = bounceCandidates.get(random.nextInt(bounceCandidates.size())); + bouncing.add(toBounce); + replicasFromBouncedReplicaSets.addAll(replicas); + } + j++; + } + + int majority = newCms.size() / 2 + 1; + String msg = String.format("In a %d node cluster, %d nodes picked for bounce, %d out of %d CMS nodes%n", + metadata.directory.peerIds().size(), + bouncing.size(), Sets.intersection(newCms, bouncing).size(), newCms.size()); + if (Sets.intersection(newCms, bouncing).size() >= majority) + throw new AssertionError(msg); + else + System.out.print(msg); + } + } + } + + public static void validatePlacementsFinal(CMSTestBase.CMSSut sut, ModelState modelState) throws Throwable + { + ClusterMetadata actualMetadata = sut.service.metadata(); + ReplicationParams replication = actualMetadata.schema.getKeyspaces().get("test").get().params.replication; + Assert.assertEquals(replication, sut.rf.asKeyspaceParams().replication); + match(actualMetadata.placements.get(replication).reads, sut.rf.replicate(modelState.simulatedPlacements.nodes).asMap()); + match(actualMetadata.placements.get(replication).writes, sut.rf.replicate(modelState.simulatedPlacements.nodes).asMap()); + } + + public static void validatePlacements(CMSTestBase.CMSSut sut, ModelState modelState) throws Throwable + { + ClusterMetadata actualMetadata = sut.service.metadata(); + + ReplicationParams replication = actualMetadata.schema.getKeyspaces().get("test").get().params.replication; + Assert.assertEquals(replication, sut.rf.asKeyspaceParams().replication); + + Assert.assertEquals(modelState.simulatedPlacements.nodes.stream().map(Node::token).collect(Collectors.toSet()), + actualMetadata.tokenMap.tokens().stream().map(t -> ((LongToken) t).token).collect(Collectors.toSet())); + + for (Map.Entry e : actualMetadata.placements.asMap().entrySet()) + { + if (!e.getKey().equals(replication)) + continue; + + DataPlacement placement = e.getValue(); + match(placement.writes, modelState.simulatedPlacements.writePlacements); + match(placement.reads, modelState.simulatedPlacements.readPlacements); + } + + validatePlacements(sut.partitioner, sut.rf, modelState, actualMetadata.placements); + } + + public static ModelChecker.Pair registerNewNode(ModelState state, CMSSut sut, int dcIdx, int rackIdx) + { + ModelState newState = state.transformer().incrementUniqueNodes().transform(); + Node node = state.nodeFactory.make(newState.uniqueNodes, dcIdx, rackIdx); + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + return pair(newState, node); + } + + private ModelChecker.Pair registerNewNode(ModelState state, CMSSut sut, int tokenIdx, int dcIdx, int rackIdx) + { + ModelState newState = state.transformer().incrementUniqueNodes().transform(); + Node node = state.nodeFactory.make(newState.uniqueNodes, dcIdx, rackIdx).withToken(tokenIdx); + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + return pair(newState, node); + } + + private Node getRemovalCandidate(ModelState state, ModelChecker.EntropySource entropySource) + { + return getCandidate(state, entropySource); + } + + private Node getMoveCandidate(ModelState state, ModelChecker.EntropySource entropySource) + { + return getCandidate(state, entropySource); + } + + private Node getCandidate(ModelState modelState, ModelChecker.EntropySource entropySource) + { + List dcs = new ArrayList<>(modelState.simulatedPlacements.rf.asMap().keySet()); + while (!dcs.isEmpty()) + { + String dc; + if (dcs.size() == 1) + dc = dcs.remove(0); + else + dc = dcs.remove(entropySource.nextInt(dcs.size() - 1)); + + Set candidates = new HashSet<>(modelState.nodesByDc.get(dc)); + for (SimulatedOperation op : modelState.inFlightOperations) + candidates.removeAll(Arrays.asList(op.nodes)); + + int rf = modelState.simulatedPlacements.rf.asMap().get(dc); + if (candidates.size() <= rf) + continue; + + Iterator iter = candidates.iterator(); + if (candidates.size() == 1) + return iter.next(); + int idx = entropySource.nextInt(candidates.size() - 1); + while (--idx >= 0) + iter.next(); + + return iter.next(); + } + + throw new IllegalStateException("Could not find a candidate for removal in " + modelState.nodesByDc); + } + + + public static String toString(Map predicted) + { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : predicted.entrySet()) + { + sb.append(e.getKey()).append('=').append(e.getValue()).append(",\n"); + } + + return sb.toString(); + } + + public static void match(PlacementForRange actual, Map> predicted) throws Throwable + { + Map, VersionedEndpoints.ForRange> actualGroups = actual.replicaGroups(); + assert predicted.size() == actualGroups.size() : + String.format("\nPredicted:\n%s(%d)" + + "\nActual:\n%s(%d)", toString(predicted), predicted.size(), toString(actual.replicaGroups()), actualGroups.size()); + + for (Map.Entry> entry : predicted.entrySet()) + { + PlacementSimulator.Range range = entry.getKey(); + List predictedNodes = entry.getValue(); + Range predictedRange = new Range(new Murmur3Partitioner.LongToken(range.start), + new Murmur3Partitioner.LongToken(range.end)); + EndpointsForRange endpointsForRange = actualGroups.get(predictedRange).get(); + assertNotNull(() -> String.format("Could not find %s in ranges %s", predictedRange, actualGroups.keySet()), + endpointsForRange); + assertEquals(() -> String.format("Predicted to have different endpoints for range %s" + + "\nExpected: %s" + + "\nActual: %s", + range, + predictedNodes.stream().sorted().collect(Collectors.toList()), + endpointsForRange.endpoints().stream().sorted().collect(Collectors.toList())), + predictedNodes.size(), endpointsForRange.size()); + for (Node node : predictedNodes) + { + assertTrue(() -> String.format("Endpoints for range %s should have contained %s, but they have not." + + "\nExpected: %s" + + "\nActual: %s.", + endpointsForRange.range(), + node.id(), + predictedNodes, + endpointsForRange.endpoints()), + endpointsForRange.endpoints().contains(InetAddressAndPort.getByAddress(InetAddress.getByName(node.id())))); + } + } + } + + + private static void assertEquals(Supplier s, Object o1, Object o2) + { + try + { + Assert.assertEquals(o1, o2); + } + catch (AssertionError e) + { + throw new AssertionError(s.get(), e); + } + } + + private static void assertNotNull(Supplier s, Object v) + { + if (v == null) + Assert.fail(s.get()); + } + + private static void assertTrue(Supplier s, boolean res) + { + if (!res) + Assert.fail(s.get()); + } + + private static ModelChecker.Pair pair(L l, R r) + { + return new ModelChecker.Pair<>(l, r); + } + + public static List toTokens(List nodes) + { + List tokens = new ArrayList<>(); + for (Node node : nodes) + tokens.add(node.longToken()); + + tokens.sort(Token::compareTo); + return tokens; + } + + public static List> toRanges(Collection ownedTokens, IPartitioner partitioner) + { + Set allTokens = new HashSet<>(); + allTokens.add(partitioner.getMinimumToken()); + allTokens.addAll(ownedTokens); + + List allTokensArr = new ArrayList<>(allTokens); + allTokensArr.sort(Token::compareTo); + allTokensArr.add(partitioner.getMinimumToken()); + + Iterator tokenIter = allTokensArr.iterator(); + Token previous = tokenIter.next(); + List> ranges = new ArrayList<>(); + while (tokenIter.hasNext()) + { + Token current = tokenIter.next(); + ranges.add(new Range<>(previous, current)); + previous = current; + } + return ranges; + } + + public static void validatePlacements(IPartitioner partitioner, + PlacementSimulator.ReplicationFactor rf, + ModelState modelState, + DataPlacements placements) + { + + Set allTokens = new HashSet<>(toTokens(modelState.currentNodes)); + for (SimulatedOperation bootstrappedNode : modelState.inFlightOperations) + allTokens.add(bootstrappedNode.nodes[0].longToken()); + + List> expectedRanges = toRanges(allTokens, partitioner); + + DataPlacement actualPlacements = placements.get(rf.asKeyspaceParams().replication); + + assertRanges(expectedRanges, actualPlacements.writes.ranges()); + assertRanges(expectedRanges, actualPlacements.reads.ranges()); + + validatePlacementsInternal(rf, modelState.inFlightOperations, expectedRanges, actualPlacements.reads, false); + validatePlacementsInternal(rf, modelState.inFlightOperations, expectedRanges, actualPlacements.writes, true); + } + + public static void assertRanges(List> l, List> r) + { + Assert.assertEquals(new TreeSet<>(l), new TreeSet<>(r)); + } + + public static void validatePlacementsInternal(PlacementSimulator.ReplicationFactor rf, List opStates, List> expectedRanges, PlacementForRange placements, boolean allowPending) + { + int overreplicated = 0; + for (Range range : expectedRanges) + { + EndpointsForRange endpointsForRange = placements.forRange(range).get(); + Directory directory = ClusterMetadata.current().directory; + Map> endpointsByDc = new TreeMap<>(); + for (Replica replica : endpointsForRange) + { + Location location = directory.location(directory.peerId(replica.endpoint())); + endpointsByDc.computeIfAbsent(location.datacenter, (k) -> new HashSet<>()) + .add(replica.endpoint()); + } + + for (Map.Entry e : rf.asMap().entrySet()) + { + int expectedRf = e.getValue(); + String dc = e.getKey(); + int actualRf = endpointsByDc.get(dc).size(); + + if (allowPending) + { + int diff = actualRf - expectedRf; + + // We may have many overreplicated ranges, but each range is overreplicated by one + assertTrue(() -> String.format("Overreplicated by %d", diff), diff == 0 || diff == 1); + overreplicated += diff; + assertTrue(() -> String.format("Expected a replication factor of %d, for range %s in dc %s but got %d", + expectedRf, range, dc, actualRf), + actualRf >= expectedRf); + } + else + { + assertEquals(() -> String.format("Expected a replication factor of %d, for range in dc %s %s but got %d", + expectedRf, range, dc, actualRf), + expectedRf, actualRf); + } + } + } + + if (allowPending && rf instanceof PlacementSimulator.SimpleReplicationFactor) + { + int bootstrappingNodes = 0; + int movingNodes = 0; + int leavingNodes = 0; + int replacedNodes = 0; + int expectedOverReplicated = 0; + + for (SimulatedOperation opState : opStates) + { + if (opState.status == SimulatedOperation.Status.STARTED) + { + if (opState.type == SimulatedOperation.Type.MOVE ) movingNodes += 1; + if (opState.type == SimulatedOperation.Type.REPLACE) replacedNodes += 1; + if (opState.type == SimulatedOperation.Type.JOIN) bootstrappingNodes += 1; + if (opState.type == SimulatedOperation.Type.LEAVE) leavingNodes += 1; + } + } + expectedOverReplicated = (replacedNodes + leavingNodes + bootstrappingNodes + movingNodes); // When the node is moving, a split range and original range to be over-replicated + // This is a trivial check for over-replication. +2 comes from wraparound ranges, since + // they have identical placements. Lower bound comes from the fact that during moves + // we may end up moving closely to the node in question, which means that we'll move by + // one range at most. Upper bound, also comes from the move, since it touches twice as many + // ranges. + // + // It is not difficult to work out when exactly when they + // are involved, but since simulator already does predicts exact placements, we leave + // this check as a failsafe for cases when simulator may have a bug identical to SUT. + int finalBootstrappingNodes = bootstrappingNodes; + int finalMovingNodes = movingNodes; + int finalLeavingNodes = leavingNodes; + int finalReplacedNodes = replacedNodes; + int finalExpectedOverReplicated = expectedOverReplicated; + int finalOverreplicated = overreplicated; + assertTrue(() -> String.format("Because there are %d nodes joining/moving/leaving/replaced (%d/%d/%d/%d) that have added themselves " + + "to write set, we expect to have at least %d ranges over-replicated, but got %d. RF %s (%s)", + finalBootstrappingNodes + finalMovingNodes + finalLeavingNodes, finalBootstrappingNodes, finalMovingNodes, finalLeavingNodes, finalReplacedNodes, finalExpectedOverReplicated, finalOverreplicated, + rf, placements), + overreplicated >= expectedOverReplicated && overreplicated <= (expectedOverReplicated * rf.total() + 2 + movingNodes * rf.total())); + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ModelChecker.java b/test/distributed/org/apache/cassandra/distributed/test/log/ModelChecker.java new file mode 100644 index 0000000000..5c997a45fb --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ModelChecker.java @@ -0,0 +1,307 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.BiConsumer; + +public class ModelChecker +{ + private final List> steps; + private final List> invariants; + private Precondition exitCondition; + private BiConsumer beforeAll; + private Pair init; + + public ModelChecker() + { + steps = new ArrayList<>(); + invariants = new ArrayList<>(); + } + + public void run() throws Throwable + { + run(Integer.MAX_VALUE); + } + + public void run(int maxSteps) throws Throwable + { + assert exitCondition != null : "Exit condition is not specified"; + assert init != null : "Initial condition is not specified"; + + Ref> state = new Ref<>(init); + EntropySource entropySource = new FakeEntropySource(new Random(88)); + if (beforeAll != null) + beforeAll.accept(state.get().l, state.get().r); + + for (int i = 0; i < maxSteps; i++) + { + if (exitCondition.test(state.get())) + return; + + // TODO: add randomisation / probability for triggering a specific step + steps.get(entropySource.nextInt(steps.size())).execute(state, entropySource.derive()); + for (Precondition invariant : invariants) + invariant.test(state.get()); + } + } + public ModelChecker init(STATE state, SUT sut) + { + this.init = new Pair<>(state, sut); + return this; + } + + public ModelChecker beforeAll(BiConsumer precondition) + { + this.beforeAll = precondition; + return this; + } + + public ModelChecker exitCondition(Precondition precondition) + { + this.exitCondition = precondition; + return this; + } + + public ModelChecker step(Precondition precondition, Step step) + { + steps.add((ref, entropySource) -> { + ref.map(state -> { + if (!precondition.test(state)) + return state; + + Pair next = step.next(state.l, state.r, entropySource); + if (next == Pair.unchanged()) + return state; + else + return next; + }); + }); + + return this; + } + + public ModelChecker invariant(Precondition invariant) + { + invariants.add(invariant); + return this; + } + + public ModelChecker step(Step step) + { + return step(Precondition.alwaysTrue(), step); + } + + public ModelChecker step(StatePrecondition precondition, ThrowingFunction step) + { + steps.add((ref, entropySource) -> { + ref.map(state -> { + if (!precondition.test(state.l)) + return state; + + STATE newState = step.apply(state.l); + if (state.l == newState) + return state; + + return state.next(newState); + }); + }); + + return this; + } + + public ModelChecker step(StatePrecondition precondition, ThrowingBiFunction step) + { + steps.add((ref, entropySource) -> { + ref.map(state -> { + if (!precondition.test(state.l)) + return state; + + STATE newState = step.apply(state.l, state.r); + if (state.l == newState) + return state; + + return state.next(newState); + }); + }); + + return this; + } + + public ModelChecker step(ThrowingFunction step) + { + return step((t, sut, entropySource) -> { + return new Pair<>(step.apply(t), sut); + }); + } + + static interface StepExecutor + { + void execute(Ref> state, EntropySource entropySource) throws Throwable; + } + + public static interface StatePrecondition + { + boolean test(STATE state) throws Throwable; + } + + public static interface Precondition + { + boolean test(STATE state, SUT sut) throws Throwable; + + default boolean test(Pair state) throws Throwable + { + return test(state.l, state.r); + } + + public static Precondition alwaysTrue() + { + return (a,b) -> true; + } + } + + public static interface Step + { + Pair next(STATE t, SUT sut, EntropySource entropySource) throws Throwable; + } + + public static interface ThrowingFunction + { + O apply(I t) throws Throwable; + } + + public static interface ThrowingBiFunction + { + O apply(I1 t1, I2 t2) throws Throwable; + } + + // Borrowed from Harry + public static interface EntropySource + { + long next(); + // We derive from entropy source here to avoid letting the step change state for other states + // For example, if you start drawing more entropy bits from one of the steps, but won't change + // other steps, their states won't change either. + EntropySource derive(); + + Random seededRandom(); + default long[] next(int n) + { + long[] next = new long[n]; + for (int i = 0; i < n; i++) + next[i] = next(); + return next; + } + + default int nextInt() + { + return RngUtils.asInt(next()); + } + + default int nextInt(int max) + { + return RngUtils.asInt(next(), max); + } + + default int nextInt(int min, int max) + { + return RngUtils.asInt(next(), min, max); + } + + default boolean nextBoolean() + { + return RngUtils.asBoolean(next()); + } + } + + public static class FakeEntropySource implements EntropySource + { + private final Random rng; + + public FakeEntropySource(Random rng) + { + this.rng = rng; + } + + public long next() + { + return rng.nextLong(); + } + + public EntropySource derive() + { + return new FakeEntropySource(new Random(rng.nextLong())); + } + + public Random seededRandom() + { + return new Random(rng.nextLong()); + } + } + + public static class Ref + { + public T ref; + + public Ref(T init) + { + this.ref = init; + } + + public T get() + { + return ref; + } + + public void set(T v) + { + this.ref = v; + } + + public void map(ThrowingFunction fn) throws Throwable + { + this.ref = fn.apply(ref); + } + } + + public static class Pair + { + private static Pair UNCHANGED = new Pair<>(null,null); + public static Pair unchanged() + { + return (Pair) UNCHANGED; + } + + public final L l; + public final R r; + + public Pair(L l, R r) + { + this.l = l; + this.r = r; + } + + public Pair next(L state) + { + return new Pair<>(state, this.r); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java new file mode 100644 index 0000000000..b1e0a6d7d5 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java @@ -0,0 +1,364 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; + +public class ModelState +{ + public final int maxClusterSize; + public final int maxConcurrency; + public final int uniqueNodes; + public final int rejected; + public final int[] cancelled; + public final int[] finished; + public final int bootstrappingCount; + public final List currentNodes; + public final Map> nodesByDc; + public final List registeredNodes; + public final List leavingNodes; + public final List movingNodes; + public final List inFlightOperations; + public final PlacementSimulator.SimulatedPlacements simulatedPlacements; + public final PlacementSimulator.NodeFactory nodeFactory; + + + public static ModelState empty(PlacementSimulator.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency) + { + return new ModelState(maxClusterSize, maxConcurrency, + 0, 0, + new int[]{ 0, 0, 0, 0 }, + new int[]{ 0, 0, 0, 0 }, + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + null, + nodeFactory); + } + + public static Map> groupByDc(List nodes) + { + // using treemap here since it is much easier to read/debug when it comes to that + Map> grouped = new TreeMap<>(); + for (PlacementSimulator.Node node : nodes) + { + grouped.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()) + .add(node); + } + return grouped; + } + + private ModelState(int maxClusterSize, + int maxConcurrency, + int uniqueNodes, + int rejected, + int[] cancelled, + int[] finished, + List currentNodes, + List registeredNodes, + List leavingNodes, + List movingNodes, + List operationStates, + PlacementSimulator.SimulatedPlacements simulatedPlacements, + PlacementSimulator.NodeFactory nodeFactory) + { + this.maxClusterSize = maxClusterSize; + this.maxConcurrency = maxConcurrency; + this.uniqueNodes = uniqueNodes; + this.rejected = rejected; + this.cancelled = cancelled; + this.finished = finished; + this.currentNodes = currentNodes; + this.registeredNodes = registeredNodes; + this.nodesByDc = groupByDc(currentNodes); + this.leavingNodes = leavingNodes; + this.movingNodes = movingNodes; + this.inFlightOperations = operationStates; + this.simulatedPlacements = simulatedPlacements; + bootstrappingCount = (int) operationStates.stream() + .filter(s -> s.type == SimulatedOperation.Type.JOIN) + .count(); + this.nodeFactory = nodeFactory; + } + + public Transformer transformer() + { + return new Transformer(this); + } + + private boolean withinConcurrencyLimit() + { + return inFlightOperations.size() < maxConcurrency; + } + + public boolean shouldBootstrap() + { + return withinConcurrencyLimit() && bootstrappingCount + currentNodes.size() < maxClusterSize; + } + + public boolean shouldLeave(PlacementSimulator.ReplicationFactor rf, Random rng) + { + return canRemove(rf) && rng.nextDouble() > 0.7; + } + + public boolean shouldMove(PlacementSimulator.ReplicationFactor rf, Random rng) + { + return canRemove(rf) && rng.nextDouble() > 0.7; + } + + public boolean shouldReplace(PlacementSimulator.ReplicationFactor rf, Random rng) + { + return canRemove(rf) && rng.nextDouble() > 0.8; + } + + private boolean canRemove(PlacementSimulator.ReplicationFactor rfs) + { + if (!withinConcurrencyLimit()) return false; + for (Map.Entry e : rfs.asMap().entrySet()) + { + String dc = e.getKey(); + int rf = e.getValue(); + List nodes = nodesByDc.get(dc); + Set nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes); + for (SimulatedOperation op : inFlightOperations) + nodesInDc.removeAll(Arrays.asList(op.nodes)); + if (nodesInDc.size() > rf) + return true; + } + return false; + } + + public boolean shouldCancel(Random rng) + { + return rng.nextDouble() > 0.95; + } + + public String toString() + { + return "ModelState{" + + "uniqueNodes=" + uniqueNodes + + ", rejectedOps=" + rejected + + ", cancelledOps=" + Arrays.toString(cancelled) + + ", finishedOps=" + Arrays.toString(finished) + + ", bootstrappedNodes=" + currentNodes + + ", leavingNodes=" + leavingNodes + + ", operationStates=" + inFlightOperations + + ", maxClusterSize=" + maxClusterSize + + ", maxConcurrency=" + maxConcurrency + + '}'; + } + + public static class Transformer + { + private final int maxClusterSize; + private final int maxConcurrency; + private int uniqueNodes; + private int rejected; + // join/replace/leave/move + private int[] cancelled; + private int[] finished; + private List currentNodes; + private List registeredNodes; + private List leavingNodes; + private List movingNodes; + private List operationStates; + private PlacementSimulator.SimulatedPlacements simulatedPlacements; + private PlacementSimulator.NodeFactory nodeFactory; + + private Transformer(ModelState source) + { + this.maxClusterSize = source.maxClusterSize; + this.maxConcurrency = source.maxConcurrency; + this.uniqueNodes = source.uniqueNodes; + this.rejected = source.rejected; + this.cancelled = source.cancelled; + this.finished = source.finished; + this.currentNodes = source.currentNodes; + this.registeredNodes = source.registeredNodes; + this.leavingNodes = source.leavingNodes; + this.movingNodes = source.movingNodes; + this.operationStates = source.inFlightOperations; + this.simulatedPlacements = source.simulatedPlacements; + this.nodeFactory = source.nodeFactory; + } + + public Transformer incrementUniqueNodes() + { + uniqueNodes++; + return this; + } + + public Transformer incrementRejected() + { + rejected++; + return this; + } + + public Transformer incrementCancelledJoin() + { + cancelled[0]++; + return this; + } + + public Transformer incrementCancelledReplace() + { + cancelled[1]++; + return this; + } + + public Transformer incrementCancelledLeave() + { + cancelled[2]++; + return this; + } + + public Transformer incrementCancelledMove() + { + cancelled[3]++; + return this; + } + + public Transformer addOperation(SimulatedOperation operation) + { + operationStates = new ArrayList<>(operationStates); + operationStates.add(operation); + return this; + } + + public Transformer removeOperation(SimulatedOperation operation) + { + operationStates = new ArrayList<>(operationStates); + operationStates.remove(operation); + return this; + } + + public Transformer withJoined(PlacementSimulator.Node node) + { + addToCluster(node); + finished[0]++; + return this; + } + + public Transformer recycleRejected(PlacementSimulator.Node node) + { + registeredNodes = new ArrayList<>(registeredNodes); + registeredNodes.add(node); + return this; + } + + public Transformer withMoved(PlacementSimulator.Node movingNode, PlacementSimulator.Node movedTo) + { + assert currentNodes.contains(movingNode) : movingNode; + List tmp = currentNodes; + currentNodes = new ArrayList<>(); + for (PlacementSimulator.Node n : tmp) + { + if (n.idx() == movingNode.idx()) + currentNodes.add(movedTo); + else + currentNodes.add(n); + } + finished[3]++; + + assert movingNodes.contains(movingNode); + movingNodes = new ArrayList<>(movingNodes); + movingNodes.remove(movingNode); + return this; + } + + private void addToCluster(PlacementSimulator.Node node) + { + // called during both join and replacement + currentNodes = new ArrayList<>(currentNodes); + currentNodes.add(node); + } + + public Transformer markMoving(PlacementSimulator.Node moving) + { + assert currentNodes.contains(moving); + movingNodes = new ArrayList<>(movingNodes); + movingNodes.add(moving); + return this; + } + + public Transformer markLeaving(PlacementSimulator.Node leaving) + { + assert currentNodes.contains(leaving); + leavingNodes = new ArrayList<>(leavingNodes); + leavingNodes.add(leaving); + return this; + } + + public Transformer withLeft(PlacementSimulator.Node node) + { + assert currentNodes.contains(node); + // for now... assassinate may change this assertion + assert leavingNodes.contains(node); + finished[1]++; + removeFromCluster(node); + return this; + } + + private void removeFromCluster(PlacementSimulator.Node node) + { + // called during both decommission and replacement + currentNodes = new ArrayList<>(currentNodes); + currentNodes.remove(node); + leavingNodes = new ArrayList<>(leavingNodes); + leavingNodes.remove(node); + } + + public Transformer withReplaced(PlacementSimulator.Node oldNode, PlacementSimulator.Node newNode) + { + addToCluster(newNode); + removeFromCluster(oldNode); + finished[1]++; + return this; + } + + public Transformer updateSimulation(PlacementSimulator.SimulatedPlacements simulatedPlacements) + { + this.simulatedPlacements = simulatedPlacements; + return this; + } + + public ModelState transform() + { + return new ModelState(maxClusterSize, + maxConcurrency, + uniqueNodes, + rejected, + cancelled, + finished, + currentNodes, + registeredNodes, + leavingNodes, + movingNodes, + operationStates, + simulatedPlacements, + nodeFactory); + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java new file mode 100644 index 0000000000..23cb98a283 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java @@ -0,0 +1,204 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.Move; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; + +/** + * Compare different operations, and make sure that executing operations such as move, bootstrap, etc., + * is consistent with bootstrapping nodes with equivalent token ownership. Useful for testing operations + * that are not yet a part of simulator, like transient replication. + */ +public class OperationalEquivalenceTest extends CMSTestBase +{ + private static final Random rng = new Random(1); + + static + { + DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + } + + // Private to this class for now as this is only a limited implementation + private static class TransientReplicationFactor extends PlacementSimulator.ReplicationFactor + { + private final PlacementSimulator.Lookup lookup = new PlacementSimulator.DefaultLookup(); + private final int dcs; + private final int full; + private final int trans; + + public TransientReplicationFactor(int dcs, int full, int trans) + { + super(dcs * (full + trans)); + this.dcs = dcs; + this.full = full; + this.trans = trans; + } + + public int dcs() + { + return dcs; + } + + public KeyspaceParams asKeyspaceParams() + { + Object[] rf = new Object[dcs * 2]; + for (int i = 0; i < dcs * 2;) + { + rf[i++] = lookup.dc(i); + rf[i++] = String.format("%s/%s", full, trans); + } + return KeyspaceParams.nts(rf); + } + + public Map asMap() + { + throw new IllegalStateException("Does not work with transient replication"); + } + + public PlacementSimulator.ReplicatedRanges replicate(PlacementSimulator.Range[] ranges, List nodes) + { + throw new IllegalStateException("Does not work with transient replication (yet)"); + } + } + + @Test + public void testMove() throws Exception + { + testMove(new PlacementSimulator.SimpleReplicationFactor(2)); + testMove(new PlacementSimulator.SimpleReplicationFactor(3)); + testMove(new PlacementSimulator.SimpleReplicationFactor(5)); + + testMove(new PlacementSimulator.NtsReplicationFactor(1, 2)); + testMove(new PlacementSimulator.NtsReplicationFactor(1, 3)); + testMove(new PlacementSimulator.NtsReplicationFactor(1, 5)); + + testMove(new PlacementSimulator.NtsReplicationFactor(3, 2)); + testMove(new PlacementSimulator.NtsReplicationFactor(3, 3)); + testMove(new PlacementSimulator.NtsReplicationFactor(3, 5)); + + testMove(new TransientReplicationFactor(3, 3, 1)); + testMove(new TransientReplicationFactor(3, 3, 2)); + } + + public void testMove(PlacementSimulator.ReplicationFactor rf) throws Exception + { + PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + + ClusterMetadata withMove = null; + List equivalentNodes = new ArrayList<>(); + int nodes = 30; + try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + AtomicInteger counter = new AtomicInteger(0); + for (int i = 0; i < nodes; i++) + { + int dc = toDc(i, rf); + PlacementSimulator.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1); + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider())); + equivalentNodes.add(node); + } + + PlacementSimulator.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size())); + PlacementSimulator.Node moved = toMove.withNewToken(); + equivalentNodes.set(equivalentNodes.indexOf(toMove), moved); + + Move plan = SimulatedOperation.prepareMove(sut, toMove, moved.longToken()).get(); + Iterator iter = SimulatedOperation.toIter(sut.service, plan.startMove, plan.midMove, plan.finishMove); + while (iter.hasNext()) + iter.next(); + + withMove = ClusterMetadata.current(); + } + + assertPlacements(simulateAndCompare(rf, equivalentNodes).placements, + withMove.placements); + } + + private static ClusterMetadata simulateAndCompare(PlacementSimulator.ReplicationFactor rf, List nodes) throws Exception + { + Collections.shuffle(nodes, rng); + try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + for (PlacementSimulator.Node node : nodes) + { + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider())); + } + + return ClusterMetadata.current(); + } + } + + private static void assertPlacements(DataPlacements l, DataPlacements r) + { + l.forEach((params, lPlacement) -> { + DataPlacement rPlacement = r.get(params); + lPlacement.reads.replicaGroups().forEach((range, lReplicas) -> { + EndpointsForRange rReplicas = rPlacement.reads.forRange(range).get(); + + Assert.assertEquals(toReplicas(lReplicas.get()), + toReplicas(rReplicas)); + }); + + lPlacement.writes.replicaGroups().forEach((range, lReplicas) -> { + EndpointsForRange rReplicas = rPlacement.writes.forRange(range).get(); + + Assert.assertEquals(toReplicas(lReplicas.get()), + toReplicas(rReplicas)); + + }); + }); + } + + public static List toReplicas(EndpointsForRange ep) + { + return ep.stream().sorted(Replica::compareTo).collect(Collectors.toList()); + } + private static int toDc(int i, PlacementSimulator.ReplicationFactor rf) + { + return (i % rf.dcs()) + 1; + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PauseCommitsTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/PauseCommitsTest.java new file mode 100644 index 0000000000..b522a22d21 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/PauseCommitsTest.java @@ -0,0 +1,56 @@ +/* + * 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.log; + +import java.io.IOException; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.ClusterMetadataService; + +import static org.junit.Assert.fail; + + +public class PauseCommitsTest extends TestBaseImpl +{ + @Test + public void pauseCommitsTest() throws IOException + { + try (Cluster cluster = init(builder().withNodes(2) + .start())) + { + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().pauseCommits()); + cluster.schemaChange(withKeyspace("create table %s.y (id int primary key)")); + try + { + cluster.coordinator(2).execute(withKeyspace("create table %s.z (id int primary key)"), ConsistencyLevel.ONE); + fail(); + } + catch (IllegalStateException e) + { + //ignore + } + cluster.get(2).runOnInstance(() -> ClusterMetadataService.instance().resumeCommits()); + cluster.coordinator(2).execute(withKeyspace("create table %s.z (id int primary key)"), ConsistencyLevel.ONE); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java new file mode 100644 index 0000000000..4d44c3b775 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java @@ -0,0 +1,1789 @@ +/* + * 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.log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.UnknownHostException; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.Assert; + +import harry.generators.PCGFastPure; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.locator.InetAddressAndPort; +; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; + +/** + * A small class that helps to avoid doing mental arithmetics on ranges. + */ +public class PlacementSimulator +{ + @SuppressWarnings("unused") // for debugging convenience + public static List readableTokens(int number) + { + final List longs; + longs = new ArrayList<>(); + for (int i = 0; i < number; i++) + { + longs.add((i + 1) * 100L); + } + Collections.shuffle(longs, new Random(1)); + + return longs; + } + + public static DebugLog debug = new DebugLog(); + + public static class SimulatedPlacements + { + public final ReplicationFactor rf; + public final List nodes; + public final NavigableMap> readPlacements; + public final NavigableMap> writePlacements; + // Stashed states are steps required to finish the operation. For example, in case of + // bootstrap, this could be adding replicas to write (and then read) sets after splitting ranges. + public final List stashedStates; + + public SimulatedPlacements(ReplicationFactor rf, + List nodes, + NavigableMap> readPlacements, + NavigableMap> writePlacements, + List stashedStates) + { + this.rf = rf; + this.nodes = nodes; + this.readPlacements = readPlacements; + this.writePlacements = writePlacements; + this.stashedStates = stashedStates; + } + + public SimulatedPlacements withNodes(List newNodes) + { + return new SimulatedPlacements(rf, newNodes, readPlacements, writePlacements, stashedStates); + } + + public SimulatedPlacements withReadPlacements(NavigableMap> newReadPlacements) + { + return new SimulatedPlacements(rf, nodes, newReadPlacements, writePlacements, stashedStates); + } + + public SimulatedPlacements withWritePlacements(NavigableMap> newWritePlacements) + { + return new SimulatedPlacements(rf, nodes, readPlacements, newWritePlacements, stashedStates); + } + + public SimulatedPlacements withStashed(Transformations steps) + { + List newStashed = new ArrayList<>(); + newStashed.addAll(stashedStates); + newStashed.add(steps); + return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStashed); + } + + private SimulatedPlacements withoutStashed(Transformations finished) + { + List newStates = new ArrayList<>(); + for (Transformations s : stashedStates) + if (s != finished) + newStates.add(s); + return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStates); + } + + public boolean isWriteTargetFor(long token, Predicate predicate) + { + return writePlacementsFor(token).stream().anyMatch(predicate); + } + + public boolean isReadReplicaFor(long token, Predicate predicate) + { + return readReplicasFor(token).stream().anyMatch(predicate); + } + + public boolean isReadReplicaFor(long minToken, long maxToken, Predicate predicate) + { + return readReplicasFor(minToken, maxToken).stream().anyMatch(predicate); + } + + public List writePlacementsFor(long token) + { + for (Map.Entry> e : writePlacements.entrySet()) + { + if (e.getKey().contains(token)) + return e.getValue(); + } + + throw new AssertionError(); + } + + public List readReplicasFor(long minToken, long maxToken) + { + for (Map.Entry> e : readPlacements.entrySet()) + { + if (e.getKey().contains(minToken, maxToken)) + return e.getValue(); + } + + throw new AssertionError(); + } + + + public List readReplicasFor(long token) + { + for (Map.Entry> e : readPlacements.entrySet()) + { + if (e.getKey().contains(token)) + return e.getValue(); + } + + throw new AssertionError(); + } + + public String toString() + { + return "ModelState{" + + "\nrf=" + rf + + "\nnodes=" + nodes + + "\nreadPlacements=\n" + placementsToString(readPlacements) + + "\nwritePlacements=\n" + placementsToString(writePlacements) + + '}'; + } + } + + public interface SimulatedPlacementHolder + { + SimulatedPlacements get(); + + /** + * Applies _one_ of the transformations given to the current state, returning the resulting state. + * _Does_ set the state within the holder as well. + */ + SimulatedPlacements applyNext(Transformations fn); + SimulatedPlacementHolder set(SimulatedPlacements placements); + SimulatedPlacementHolder fork(); + } + + public static class RefSimulatedPlacementHolder implements SimulatedPlacementHolder + { + private SimulatedPlacements state; + + public RefSimulatedPlacementHolder(SimulatedPlacements state) + { + this.state = state; + } + + public SimulatedPlacements get() + { + return state; + } + + public SimulatedPlacements applyNext(Transformations fn) + { + return state = fn.advance(state); + } + + public SimulatedPlacementHolder set(SimulatedPlacements newState) + { + state = newState; + return this; + } + + public SimulatedPlacementHolder fork() + { + return new RefSimulatedPlacementHolder(state); + } + } + + public static class Transformation + { + private Function apply; + private Function revert; + + Transformation(Function apply, + Function revert) + { + this.apply = apply; + this.revert = revert; + } + + public Transformation prepare(Function apply, + Function revert) + { + this.apply = apply; + this.revert = revert; + return this; + } + } + + public static class Transformations + { + private final List steps = new ArrayList<>(); + private int idx = 0; + + public void add(Transformation step) + { + steps.add(step); + } + + public boolean hasNext() + { + return idx < steps.size(); + } + + public SimulatedPlacements advance(SimulatedPlacements prev) + { + if (idx >= steps.size()) + throw new IllegalStateException("Cannot advance transformations, no more steps remaining"); + + SimulatedPlacements next = steps.get(idx++).apply.apply(prev); + if (!hasNext()) + next = next.withoutStashed(this); + + return next; + } + + public boolean hasPrevious() + { + return idx > 0; + } + + public SimulatedPlacements revertPublishedEffects(SimulatedPlacements state) + { + while (hasPrevious()) + state = steps.get(--idx).revert.apply(state); + + return state.withoutStashed(this); + } + } + + public static SimulatedPlacements joinFully(SimulatedPlacements baseState, Node node) + { + Transformations transformations = join(baseState, node); + baseState = baseState.withStashed(transformations); + + while (transformations.hasNext()) + baseState = transformations.advance(baseState); + + return baseState; + } + + /** + * Diff-based bootstrap (very close implementation-wise to what production code does) + */ + public static Transformations join(SimulatedPlacements baseState, Node bootstrappingNode) + { + long token = bootstrappingNode.token(); + List splitNodes = split(baseState.nodes, token); + Map> maximalStateWithPlacement = baseState.rf.replicate(move(splitNodes, token, bootstrappingNode)).placementsForRange; + + NavigableMap> splitReadPlacements = baseState.rf.replicate(splitNodes).placementsForRange; + NavigableMap> splitWritePlacements = baseState.rf.replicate(splitNodes).placementsForRange; + + Map> allWriteCommands = diff(splitWritePlacements, maximalStateWithPlacement); + Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); + Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); + Map> readCommands = diff(splitReadPlacements, maximalStateWithPlacement); + + Transformations steps = new Transformations(); + + steps.add(new Transformation( + (model) -> { // apply + // add the new node to the system and split ranges according to its token, while retaining current + // placement. This step will always be executed immediately, whereas subsequent steps may be deferred + debug.log("Splitting ranges to prepare for join of " + bootstrappingNode + "\n"); + return model.withReadPlacements(splitReplicated(baseState.readPlacements, token)) + .withWritePlacements(splitReplicated(baseState.writePlacements, token)); + }, + (model) -> { // revert + // final stage of reverting a join is to undo the range splits performed by preparing the operation + debug.log("Reverting range splits from prepare-join of " + bootstrappingNode + "\n"); + return model.withWritePlacements(mergeReplicated(model.writePlacements, token)) + .withReadPlacements(mergeReplicated(model.readPlacements, token)); + }) + ); + + // Step 1: add new node as a write replica to all ranges it is gaining + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing start-join of " + bootstrappingNode + "\n"); + debug.log(String.format("Commands for step 1 of bootstrap of %s.\n" + + "\twriteModifications=\n%s", + bootstrappingNode, diffsToString(step1WriteCommands))); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); + }, + (model) -> { // revert + debug.log("Reverting start-join of " + bootstrappingNode + "\n"); + Map> inverted = map(step1WriteCommands, Diff::invert); + debug.log("Commands for reverting step 1 of bootstrap of %s.\n" + + "\twriteModifications=\n%s", + bootstrappingNode, diffsToString(inverted)); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); + }) + ); + + // Step 2: add new node as a read replica to the ranges it is gaining; remove old node from reads at the same time + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing mid-join of " + bootstrappingNode + "\n"); + debug.log(String.format("Commands for step 2 of bootstrap of %s.\n" + + "\treadCommands=\n%s", + bootstrappingNode, diffsToString(readCommands))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); + }, + (model) -> { // revert + debug.log("Reverting mid-join of " + bootstrappingNode + "\n"); + Map> inverted = map(readCommands, Diff::invert); + debug.log(String.format("Commands for reverting step 2 of bootstrap of %s.\n" + + "\treadCommands=\n%s", + bootstrappingNode, diffsToString(inverted))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); + }) + ); + + + // Step 3: finally remove the old node from writes + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing finish-join of " + bootstrappingNode + "\n"); + debug.log(String.format("Commands for step 3 of bootstrap of %s.\n" + + "\twriteModifications=\n%s", + bootstrappingNode, + diffsToString(step3WriteCommands))); + List newNodes = new ArrayList<>(model.nodes); + newNodes.add(bootstrappingNode); + newNodes.sort(Node::compareTo); + return model.withNodes(newNodes) + .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); + }, + (model) -> { //revert + throw new IllegalStateException("Can't revert finish-join of " + bootstrappingNode + ", operation is already complete\n"); + }) + ); + + debug.log("Planned bootstrap of " + bootstrappingNode + "\n"); + return steps; + } + + public static Transformations move(SimulatedPlacements baseState, Node movingNode, long newToken) + { + List origNodes = new ArrayList<>(baseState.nodes); + List finalNodes = new ArrayList<>(); + for (int i = 0; i < origNodes.size(); i++) + { + if (origNodes.get(i).nodeIdx == movingNode.nodeIdx) + continue; + finalNodes.add(origNodes.get(i)); + } + finalNodes.add(movingNode.overrideToken(newToken)); + finalNodes.sort(Node::compareTo); + + Map> start = splitReplicated(baseState.rf.replicate(origNodes).placementsForRange, newToken); + Map> end = splitReplicated(baseState.rf.replicate(finalNodes).placementsForRange, movingNode.token()); + + Map> fromStartToEnd = diff(start, end); + + Transformations steps = new Transformations(); + + // Step 1: Prepare Move, + steps.add(new Transformation( + (model) -> { // apply + debug.log(String.format("Splitting ranges to prepare for move of %s to %d\n", movingNode, newToken)); + return model.withReadPlacements(splitReplicated(model.readPlacements, newToken)) + .withWritePlacements(splitReplicated(model.writePlacements, newToken)); + }, + (model) -> { // revert + debug.log(String.format("Reverting range splits from prepare move of %s to %d\n", movingNode, newToken)); + return model.withWritePlacements(mergeReplicated(model.writePlacements, newToken)) + .withReadPlacements(mergeReplicated(model.readPlacements, newToken)); + }) + ); + + // Step 2: Start Move, add all potential owners to write quorums + steps.add(new Transformation( + (model) -> { // apply + Map> diff = map(fromStartToEnd, Diff::onlyAdditions); + debug.log("Executing start-move of " + movingNode + "\n"); + debug.log(String.format("Commands for step 1 of move of %s to %d.\n" + + "\twriteModifications=\n%s", + movingNode, newToken, diffsToString(diff))); + + NavigableMap> placements = model.writePlacements; + placements = PlacementSimulator.apply(placements, diff); + return model.withWritePlacements(placements); + }, + (model) -> { // revert + debug.log("Reverting start-move of " + movingNode + "\n"); + Map> diff = map(fromStartToEnd, Diff::onlyAdditions); + Map> inverted = map(diff, Diff::invert); + debug.log(String.format("Commands for reverting step 1 of move of %s to %d.\n" + + "\twriteModifications=\n%s", + movingNode, newToken, diffsToString(inverted))); + + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); + } + )); + // Step 3: Mid Move, remove all nodes that are losing ranges from read quorums, add all nodes gaining ranges to read quorums + steps.add(new Transformation( + (model) -> { + debug.log("Executing mid-move of " + movingNode + "\n"); + debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + + "\treadModifications=\n%s", + movingNode, newToken, diffsToString(fromStartToEnd))); + + NavigableMap> placements = model.readPlacements; + placements = PlacementSimulator.apply(placements, fromStartToEnd); + return model.withReadPlacements(placements); + }, + (model) -> { + NavigableMap> placements = PlacementSimulator.apply(model.readPlacements, map(fromStartToEnd, Diff::invert)); + return model.withReadPlacements(placements); + }) + ); + + // Step 4: Finish Move, remove all nodes that are losing ranges from write quorums + steps.add(new Transformation( + (model) -> { + Map> diff = map(fromStartToEnd, Diff::onlyRemovals); + + debug.log("Executing finish-move of " + movingNode + "\n"); + debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + + "\twriteModifications=\n%s", + movingNode, newToken, diffsToString(diff))); + + List currentNodes = new ArrayList<>(model.nodes); + List newNodes = new ArrayList<>(); + for (int i = 0; i < currentNodes.size(); i++) + { + if (currentNodes.get(i).idx() == movingNode.idx()) + continue; + newNodes.add(currentNodes.get(i)); + } + newNodes.add(movingNode.overrideToken(newToken)); + newNodes.sort(Node::compareTo); + + Map> writePlacements = model.writePlacements; + writePlacements = PlacementSimulator.apply(writePlacements, diff); + + return model.withWritePlacements(mergeReplicated(writePlacements, movingNode.token())) + .withReadPlacements(mergeReplicated(model.readPlacements, movingNode.token())) + .withNodes(newNodes); + }, + (model) -> { + throw new IllegalStateException(String.format("Can't revert finish-move of %d, operation is already complete", newToken)); + }) + ); + + return steps; + } + + public static Transformations leave(SimulatedPlacements baseState, Node toRemove) + { + // calculate current placements - this is start state + Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; + + List afterLeaveNodes = new ArrayList<>(baseState.nodes); + afterLeaveNodes.remove(toRemove); + // calculate placements based on existing ranges but final set of nodes - this is end state + Map> end = baseState.rf.replicate(toRanges(baseState.nodes), afterLeaveNodes).placementsForRange; + // maximal state is union of start & end + + Map> allWriteCommands = diff(start, end); + Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); + Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); + Map> readCommands = diff(start, end); + Transformations steps = new Transformations(); + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing start-leave of " + toRemove + "\n"); + debug.log(String.format("Commands for step 1 of decommission of %s.\n" + + "\twriteModifications=\n%s", + toRemove, diffsToString(step1WriteCommands))); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); + }, + (model) -> { // revert + debug.log("Reverting start-leave of " + toRemove + "\n"); + Map> inverted = map(step1WriteCommands, Diff::invert); + debug.log(String.format("Commands for reverting step 1 of decommission of %s.\n" + + "\twriteModifications=\n%s", + toRemove, diffsToString(inverted))); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); + }) + ); + + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing mid-leave of " + toRemove + "\n"); + debug.log(String.format("Commands for step 2 of decommission of %s.\n" + + "\treadModifications=\n%s", + toRemove, + diffsToString(readCommands))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); + }, + (model) -> { // revert + debug.log("Reverting mid-leave of " + toRemove + "\n"); + Map> inverted = map(readCommands, Diff::invert); + debug.log(String.format("Commands for reverting step 2 of decommission of %s.\n" + + "\treadModifications=\n%s", + toRemove, + diffsToString(inverted))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); + }) + ); + + steps.add(new Transformation( + (model) -> { // apply + debug.log("Executing finish-leave decommission of " + toRemove + "\n"); + debug.log(String.format("Commands for step 3 of decommission of %s.\n" + + "\twriteModifications=\n%s", + toRemove, + diffsToString(step3WriteCommands))); + List newNodes = new ArrayList<>(model.nodes); + newNodes.remove(toRemove); + newNodes.sort(Node::compareTo); + Map> writes = PlacementSimulator.apply(model.writePlacements, step3WriteCommands); + return model.withReadPlacements(mergeReplicated(model.readPlacements, toRemove.token())) + .withWritePlacements(mergeReplicated(writes, toRemove.token())) + .withNodes(newNodes); + }, + (model) -> { // revert + throw new IllegalStateException("Can't revert finish-leave of " + toRemove + ", operation is already complete\n"); + })); + + debug.log("Planned decommission of " + toRemove + "\n"); + return steps; + } + + public static Transformations replace(SimulatedPlacements baseState, Node toReplace, Node replacement) + { + Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; + Map> allCommands = new TreeMap<>(); + start.forEach((range, nodes) -> { + if (nodes.contains(toReplace)) + { + allCommands.put(range, new Diff<>(Collections.singletonList(replacement), + Collections.singletonList(toReplace))); + } + }); + Map> step1WriteCommands = map(allCommands, Diff::onlyAdditions); + Map> step3WriteCommands = map(allCommands, Diff::onlyRemovals); + Map> readCommands = allCommands; + Transformations steps = new Transformations(); + steps.add(new Transformation( + (model) -> { // apply + debug.log(String.format("Executing start-replace of %s for %s\n", replacement, toReplace)); + debug.log(String.format("Commands for step 1 of bootstrap of %s for replacement of %s.\n" + + "\twriteModifications=\n%s", + replacement, toReplace, diffsToString(step1WriteCommands))); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); + }, + (model) -> { // revert + debug.log(String.format("Reverting start-replace of %s for %s\n", replacement, toReplace)); + Map> inverted = map(step1WriteCommands, Diff::invert); + debug.log(String.format("Commands for reverting step 1 of bootstrap of %s for replacement of %s.\n" + + "\twriteModifications=\n%s", + replacement, toReplace, diffsToString(inverted))); + return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); + }) + ); + + steps.add(new Transformation( + (model) -> { // apply + debug.log(String.format("Executing mid-replace of %s for %s\n", replacement, toReplace)); + debug.log(String.format("Commands for step 2 of bootstrap of %s for replacement of %s.\n" + + "\treadModifications=\n%s", + replacement, toReplace, + diffsToString(readCommands))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); + }, + (model) -> { // revert + debug.log(String.format("Reverting mid-replace of %s for %s\n", replacement, toReplace)); + Map> inverted = map(readCommands, Diff::invert); + debug.log(String.format("Commands for reverting step 2 of bootstrap of %s for replacement of %s.\n" + + "\treadModifications=\n%s", + replacement, toReplace, + diffsToString(inverted))); + return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); + }) + ); + + steps.add(new Transformation( + (model) -> { // apply + debug.log(String.format("Executing finish-replace of %s for %s\n", replacement, toReplace)); + debug.log(String.format("Commands for step 3 of bootstrap of %sfor replacement of %s.\n" + + "\twriteModifications=\n%s", + replacement, toReplace, + diffsToString(step3WriteCommands))); + List newNodes = new ArrayList<>(model.nodes); + newNodes.remove(toReplace); + newNodes.add(replacement); + newNodes.sort(Node::compareTo); + return model.withNodes(newNodes) + .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); + }, + (model) -> { // revert + throw new IllegalStateException(String.format("Can't revert finish-replace of %s for %s, operation is already complete\n", replacement, toReplace)); + }) + ); + + debug.log(String.format("Planned bootstrap of %s for replacement of %s\n", replacement, toReplace)); + return steps; + } + + public static void assertPlacements(SimulatedPlacements placements, Map> r, Map> w) + { + assertRanges(r, placements.readPlacements); + assertRanges(w, placements.writePlacements); + } + + public static void assertRanges(Map> expected, Map> actual) + { + Assert.assertEquals(expected.keySet(), actual.keySet()); + expected.forEach((k, v) -> { + // When comparing replica sets, we only care about the endpoint (i.e. the node.id). For the purpose + // of simulation, during split operations we duplicate the node holding the range being split as if giving + // it two tokens, the original one and the split point. e.g. With N1@100, N2@200 then splitting at 150, + // we will end up with (100, 150] -> N2@150 and (150, 200] -> N2@200. As this is purely an artefact of the + // bootstrap_diffBased implementation and the real code doesn't do this, only the endpoint matters for + // correctness, so we limit this comparison to endpoints only. + Assert.assertEquals(String.format("For key: %s\n", k), + expected.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList()), + actual.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList())); + }); + } + + public static boolean containsAll(Set a, Set b) + { + if (a.isEmpty() && !b.isEmpty()) + return false; // empty set does not contain all entries of a non-empty one + for (T v : b) + if (!a.contains(v)) + return false; + + return true; + } + + /** + * Applies a given diff to the placement map + */ + public static NavigableMap> apply(Map> orig, Map> diff) + { + assert containsAll(orig.keySet(), diff.keySet()) : String.format("Can't apply diff to a map with different sets of keys:" + + "\nOrig ks: %s" + + "\nDiff ks: %s" + + "\nDiff: %s", + orig.keySet(), diff.keySet(), diff); + NavigableMap> res = new TreeMap<>(); + for (Map.Entry> entry : orig.entrySet()) + { + Range range = entry.getKey(); + if (diff.containsKey(range)) + res.put(range, apply(entry.getValue(), diff.get(range))); + else + res.put(range, entry.getValue()); + } + return Collections.unmodifiableNavigableMap(res); + } + + /** + * Apply diff to a list of nodes + */ + public static List apply(List nodes, Diff diff) + { + Set tmp = new HashSet<>(nodes); + tmp.addAll(diff.additions); + for (Node removal : diff.removals) + tmp.remove(removal); + List newNodes = new ArrayList<>(tmp); + newNodes.sort(Node::compareTo); + return Collections.unmodifiableList(newNodes); + } + + /** + * Diff two placement maps + */ + public static Map> diff(Map> l, Map> r) + { + assert l.keySet().equals(r.keySet()) : String.format("Can't diff events from different bases %s %s", l.keySet(), r.keySet()); + Map> diff = new TreeMap<>(); + for (Map.Entry> entry : l.entrySet()) + { + Range range = entry.getKey(); + Diff d = diff(entry.getValue(), r.get(range)); + if (!d.removals.isEmpty() || !d.additions.isEmpty()) + diff.put(range, d); + } + return Collections.unmodifiableMap(diff); + } + + public static Map map(Map diff, Function fn) + { + Map newDiff = new TreeMap<>(); + for (Map.Entry entry : diff.entrySet()) + { + T newV = fn.apply(entry.getValue()); + if (newV != null) + newDiff.put(entry.getKey(), newV); + } + return Collections.unmodifiableMap(newDiff); + } + + public static List map(List coll, Function map) + { + List newColl = new ArrayList<>(coll); + for (T v : coll) + newColl.add(map.apply(v)); + return newColl; + } + + /** + * Produce a diff (i.e. set of additions/subtractions that should be applied to the list of nodes in order to produce + * r from l) + */ + public static Diff diff(List l, List r) + { + // additions things present in r but not in l + List additions = new ArrayList<>(); + // removals are things present in l but not r + List removals = new ArrayList<>(); + + for (Node i : r) + { + boolean isPresentInL = false; + for (Node j : l) + { + if (i.equals(j)) + { + isPresentInL = true; + break; + } + } + + if (!isPresentInL) + additions.add(i); + } + + for (Node i : l) + { + boolean isPresentInR = false; + for (Node j : r) + { + if (i.equals(j)) + { + isPresentInR = true; + break; + } + } + + if (!isPresentInR) + removals.add(i); + } + return new Diff<>(additions, removals); + } + + public static Map> superset(Map> l, Map> r) + { + assert l.keySet().equals(r.keySet()) : String.format("%s != %s", l.keySet(), r.keySet()); + + Map> newState = new TreeMap<>(); + for (Map.Entry> entry : l.entrySet()) + { + Range range = entry.getKey(); + Set nodes = new HashSet<>(); + nodes.addAll(entry.getValue()); + nodes.addAll(r.get(range)); + newState.put(range, new ArrayList<>(nodes)); + } + + return newState; + } + + public static NavigableMap> mergeReplicated(Map> orig, long removingToken) + { + NavigableMap> newState = new TreeMap<>(); + Iterator>> iter = orig.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry> current = iter.next(); + if (current.getKey().end == removingToken) + { + assert iter.hasNext() : "Cannot merge range, no more ranges in list"; + Map.Entry> next = iter.next(); + assert current.getValue().containsAll(next.getValue()) && current.getValue().size() == next.getValue().size() + : "Cannot merge ranges with different replica groups"; + Range merged = new Range(current.getKey().start, next.getKey().end); + newState.put(merged, current.getValue()); + } + else + { + newState.put(current.getKey(), current.getValue()); + } + } + + return newState; + } + + public static NavigableMap> splitReplicated(Map> orig, long splitAt) + { + NavigableMap> newState = new TreeMap<>(); + for (Map.Entry> entry : orig.entrySet()) + { + Range range = entry.getKey(); + if (range.contains(splitAt)) + { + newState.put(new Range(range.start, splitAt), entry.getValue()); + newState.put(new Range(splitAt, range.end), entry.getValue()); + } + else + { + newState.put(range, entry.getValue()); + } + } + return newState; + } + + /** + * "Split" the list of nodes at splitAt, without changing ownership + */ + public static List split(List nodes, long splitAt) + { + List newNodes = new ArrayList<>(); + boolean inserted = false; + Node previous = null; + for (int i = nodes.size() - 1; i >= 0; i--) + { + Node node = nodes.get(i); + if (!inserted && splitAt > node.token()) + { + // We're trying to split rightmost range + if (previous == null) + { + newNodes.add(nodes.get(0).overrideToken(splitAt)); + } + else + { + newNodes.add(previous.overrideToken(splitAt)); + } + inserted = true; + } + + newNodes.add(node); + previous = node; + } + + // Leftmost is split + if (!inserted) + newNodes.add(previous.overrideToken(splitAt)); + + newNodes.sort(Node::compareTo); + return Collections.unmodifiableList(newNodes); + } + + /** + * Change the ownership of the freshly split token + */ + public static List move(List nodes, long tokenToMove, Node newOwner) + { + List newNodes = new ArrayList<>(); + for (Node node : nodes) + { + if (node.token() == tokenToMove) + newNodes.add(newOwner.overrideToken(tokenToMove)); + else + newNodes.add(node); + } + newNodes.sort(Node::compareTo); + return Collections.unmodifiableList(newNodes); + } + + public static List filter(List nodes, Predicate pred) + { + List newNodes = new ArrayList<>(); + for (Node node : nodes) + { + if (pred.test(node)) + newNodes.add(node); + } + newNodes.sort(Node::compareTo); + return Collections.unmodifiableList(newNodes); + } + + /** + * Replicate ranges to rf nodes. + */ + private static ReplicatedRanges combine(NavigableMap>> orig) + { + + Range[] ranges = new Range[orig.size()]; + int idx = 0; + NavigableMap> flattened = new TreeMap<>(); + for (Map.Entry>> e : orig.entrySet()) + { + List placementsForRange = new ArrayList<>(); + for (List v : e.getValue().values()) + placementsForRange.addAll(v); + ranges[idx++] = e.getKey(); + flattened.put(e.getKey(), placementsForRange); + } + return new ReplicatedRanges(ranges, flattened); + } + + public static class ReplicatedRanges + { + private final Range[] ranges; + private final NavigableMap> placementsForRange; + + public ReplicatedRanges(Range[] ranges, NavigableMap> placementsForRange) + { + this.ranges = ranges; + this.placementsForRange = placementsForRange; + } + + public List replicasFor(long token) + { + int idx = indexedBinarySearch(ranges, range -> { + // exclusive start, so token at the start belongs to a lower range + if (token <= range.start) + return 1; + // ie token > start && token <= end + if (token <= range.end ||range.end == Long.MIN_VALUE) + return 0; + + return -1; + }); + assert idx >= 0 : String.format("Somehow ranges %s do not contain token %d", Arrays.toString(ranges), token); + return placementsForRange.get(ranges[idx]); + } + + public NavigableMap> asMap() + { + return placementsForRange; + } + + private static int indexedBinarySearch(T[] arr, CompareTo comparator) + { + int low = 0; + int high = arr.length - 1; + + while (low <= high) + { + int mid = (low + high) >>> 1; + T midEl = arr[mid]; + int cmp = comparator.compareTo(midEl); + + if (cmp < 0) + low = mid + 1; + else if (cmp > 0) + high = mid - 1; + else + return mid; + } + return -(low + 1); // key not found + } + } + + public interface CompareTo + { + int compareTo(V v); + } + + private static , T1, T2> Map mapValues(Map allDCs, Function map) + { + NavigableMap res = new TreeMap<>(); + for (Map.Entry e : allDCs.entrySet()) + { + res.put(e.getKey(), map.apply(e.getValue())); + } + return res; + } + + public static Map> nodesByDC(List nodes) + { + Map> nodesByDC = new HashMap<>(); + for (Node node : nodes) + nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); + + return nodesByDC; + } + + public static Map> racksByDC(List nodes) + { + Map> racksByDC = new HashMap<>(); + for (Node node : nodes) + racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack()); + + return racksByDC; + } + + private static final class DatacenterNodes + { + private final List nodes = new ArrayList<>(); + private final Set racks = new HashSet<>(); + + /** Number of replicas left to fill from this DC. */ + int rfLeft; + int acceptableRackRepeats; + + public DatacenterNodes copy() + { + return new DatacenterNodes(rfLeft, acceptableRackRepeats); + } + + DatacenterNodes(int rf, + int rackCount, + int nodeCount) + { + this.rfLeft = Math.min(rf, nodeCount); + acceptableRackRepeats = rf - rackCount; + } + + // for copying + DatacenterNodes(int rfLeft, int acceptableRackRepeats) + { + this.rfLeft = rfLeft; + this.acceptableRackRepeats = acceptableRackRepeats; + } + + boolean addAndCheckIfDone(Node node, Location location) + { + if (done()) + return false; + + if (nodes.contains(node)) + // Cannot repeat a node. + return false; + + if (racks.add(location)) + { + // New rack. + --rfLeft; + nodes.add(node); + return done(); + } + if (acceptableRackRepeats <= 0) + // There must be rfLeft distinct racks left, do not add any more rack repeats. + return false; + + nodes.add(node); + + // Added a node that is from an already met rack to match RF when there aren't enough racks. + --acceptableRackRepeats; + --rfLeft; + return done(); + } + + boolean done() + { + assert rfLeft >= 0; + return rfLeft == 0; + } + } + + + public static void addIfUnique(List nodes, Set names, Node node) + { + if (names.contains(node.idx())) + return; + nodes.add(node); + names.add(node.idx()); + } + + /** + * Finds a primary replica + */ + public static int primaryReplica(List nodes, Range range) + { + for (int i = 0; i < nodes.size(); i++) + { + if (range.end != Long.MIN_VALUE && nodes.get(i).token() >= range.end) + return i; + } + return -1; + } + + /** + * Generates token ranges from the list of nodes + */ + public static Range[] toRanges(List nodes) + { + List tokens = new ArrayList<>(); + for (Node node : nodes) + tokens.add(node.token()); + tokens.add(Long.MIN_VALUE); + tokens.sort(Long::compareTo); + + Range[] ranges = new Range[nodes.size() + 1]; + long prev = tokens.get(0); + int cnt = 0; + for (int i = 1; i < tokens.size(); i++) + { + long current = tokens.get(i); + ranges[cnt++] = new Range(prev, current); + prev = current; + } + ranges[ranges.length - 1] = new Range(prev, Long.MIN_VALUE); + return ranges; + + } + + public static class Diff { + public final List additions; + public final List removals; + + public Diff(List additions, List removals) + { + this.additions = additions; + this.removals = removals; + } + + public String toString() + { + return "Diff{" + + "additions=" + additions + + ", removals=" + removals + + '}'; + } + + public Diff onlyAdditions() + { + if (additions.isEmpty()) return null; + return new Diff<>(additions, Collections.emptyList()); + } + + public Diff onlyRemovals() + { + if (removals.isEmpty()) return null; + return new Diff<>(Collections.emptyList(), removals); + } + + public Diff invert() + { + // invert removals & additions + return new Diff<>(removals, additions); + } + } + + + /** + * A Range is responsible for the tokens between (start, end]. + */ + public static class Range implements Comparable + { + public final long start; + public final long end; + + public Range(long start, long end) + { + assert end > start || end == Long.MIN_VALUE : String.format("Start (%d) should be smaller than end (%d)", start, end); + this.start = start; + this.end = end; + } + + public boolean contains(long min, long max) + { + assert max > min; + return min > start && (max <= end || end == Long.MIN_VALUE); + } + + public boolean contains(long token) + { + return token > start && (token <= end || end == Long.MIN_VALUE); + } + + public int compareTo(Range o) + { + int res = Long.compare(start, o.start); + if (res == 0) + return Long.compare(end, o.end); + return res; + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range range = (Range) o; + return start == range.start && end == range.end; + } + + public int hashCode() + { + return Objects.hash(start, end); + } + + public String toString() + { + return "(" + + "" + (start == Long.MIN_VALUE ? "MIN" : start) + + ", " + (end == Long.MIN_VALUE ? "MIN" : end) + + ']'; + } + } + + public interface Lookup + { + String id(int nodeIdx); + String dc(int dcIdx); + String rack(int rackIdx); + NodeId nodeId(int nodeIdx); + long token(int tokenIdx); + Lookup forceToken(int tokenIdx, long token); + InetAddressAndPort addr(int idx); + void reset(); + } + + public static class DefaultLookup implements Lookup + { + protected final Map overrides = new HashMap<>(2); + + public String id(int nodeIdx) + { + return String.format("127.0.%d.%d", nodeIdx / 256, nodeIdx % 256); + } + + public NodeId nodeId(int nodeIdx) + { + return ClusterMetadata.current().directory.peerId(addr(nodeIdx)); + } + + public long token(int tokenIdx) + { + Long override = overrides.get(tokenIdx); + if (override != null) + return override; + return PCGFastPure.next(tokenIdx, 1L); + } + + public Lookup forceToken(int tokenIdx, long token) + { + DefaultLookup newLookup = new DefaultLookup(); + newLookup.overrides.putAll(overrides); + newLookup.overrides.put(tokenIdx, token); + return newLookup; + } + + public InetAddressAndPort addr(int idx) + { + try + { + return InetAddressAndPort.getByName(id(idx)); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public void reset() + { + overrides.clear(); + } + + public String dc(int dcIdx) + { + return String.format("datacenter%d", dcIdx); + } + + public String rack(int rackIdx) + { + return String.format("rack%d", rackIdx); + } + } + + public static class HumanReadableTokensLookup extends DefaultLookup { + @Override + public long token(int tokenIdx) + { + Long override = overrides.get(tokenIdx); + if (override != null) + return override; + return tokenIdx * 100L; + } + + public Lookup forceToken(int tokenIdx, long token) + { + DefaultLookup lookup = new HumanReadableTokensLookup(); + lookup.overrides.putAll(overrides); + lookup.overrides.put(tokenIdx, token); + return lookup; + } + } + public static NodeFactory nodeFactory() + { + return new NodeFactory(new DefaultLookup()); + } + + public static NodeFactory nodeFactoryHumanReadable() + { + return new NodeFactory(new HumanReadableTokensLookup()); + } + + public static class NodeFactory implements TokenSupplier + { + private final Lookup lookup; + + public NodeFactory(Lookup lookup) + { + this.lookup = lookup; + } + + public Node make(int idx, int dc, int rack) + { + return new Node(idx, idx, dc, rack, lookup); + } + + public Lookup lookup() + { + return lookup; + } + + public Collection tokens(int i) + { + return Collections.singletonList(Long.toString(lookup.token(i))); + } + } + + public static class Node implements Comparable + { + private final int tokenIdx; + private final int nodeIdx; + private final int dcIdx; + private final int rackIdx; + private final Lookup lookup; + + private Node(int tokenIdx, int idx, int dcIdx, int rackIdx, Lookup lookup) + { + this.tokenIdx = tokenIdx; + this.nodeIdx = idx; + this.dcIdx = dcIdx; + this.rackIdx = rackIdx; + this.lookup = lookup; + } + + public InetAddressAndPort addr() + { + return lookup.addr(nodeIdx); + } + + public NodeId nodeId() + { + return lookup.nodeId(nodeIdx); + } + + public String id() + { + return lookup.id(nodeIdx); + } + + public int idx() + { + return nodeIdx; + } + + public int dcIdx() + { + return dcIdx; + } + + public int rackIdx() + { + return rackIdx; + } + + public String dc() + { + return lookup.dc(dcIdx); + } + + public String rack() + { + return lookup.rack(rackIdx); + } + + public long token() + { + return lookup.token(tokenIdx); + } + + public int tokenIdx() + { + return tokenIdx; + } + + public Murmur3Partitioner.LongToken longToken() + { + return new Murmur3Partitioner.LongToken(token()); + } + + public Node withNewToken() + { + return new Node(tokenIdx + 100_000, nodeIdx, dcIdx, rackIdx, lookup); + } + + public Node withToken(int tokenIdx) + { + return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup); + } + + public Node overrideToken(long override) + { + return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup.forceToken(tokenIdx, override)); + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || !Node.class.isAssignableFrom(o.getClass())) return false; + Node node = (Node) o; + return Objects.equals(nodeIdx, node.nodeIdx); + } + + public int hashCode() + { + return Objects.hash(nodeIdx); + } + + public int compareTo(Node o) + { + return Long.compare(token(), o.token()); + } + + public String toString() + { + return String.format("%s-%s@%d", dc(), id(), token()); + } + } + + public static String diffsToString(Map> placements) + { + StringBuilder builder = new StringBuilder(); + for (Map.Entry> e : placements.entrySet()) + { + builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); + } + return builder.toString(); + } + + public static String placementsToString(Map> placements) + { + StringBuilder builder = new StringBuilder(); + for (Map.Entry> e : placements.entrySet()) + { + builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); + } + return builder.toString(); + } + + public static class DebugLog + { + private final BufferedWriter operationLog; + public DebugLog() + { + File f = new File("simulated.log"); + try + { + operationLog = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + } + + public void log(long seq, Object t) + { + log("%d: %s\n", seq, t); + } + + private void log(String format, Object... objects) + { + try + { + operationLog.write(String.format(format, objects)); + operationLog.flush(); + } + catch (IOException e) + { + // ignore + } + } + } + + public abstract static class ReplicationFactor + { + private final int nodesTotal; + + public ReplicationFactor(int total) + { + this.nodesTotal = total; + } + + public int total() + { + return nodesTotal; + } + + public abstract int dcs(); + + public abstract KeyspaceParams asKeyspaceParams(); + + public abstract Map asMap(); + + public ReplicatedRanges replicate(List nodes) + { + return replicate(toRanges(nodes), nodes); + } + public abstract ReplicatedRanges replicate(Range[] ranges, List nodes); + } + + public static class NtsReplicationFactor extends ReplicationFactor + { + private final Lookup lookup = new DefaultLookup(); + public final int[] nodesPerDc; + private KeyspaceParams keyspaceParams; + private Map map; + + public NtsReplicationFactor(int... nodesPerDc) + { + super(total(nodesPerDc)); + this.nodesPerDc = nodesPerDc; + } + + public NtsReplicationFactor(int dcs, int nodesPerDc) + { + super(dcs * nodesPerDc); + this.nodesPerDc = new int[dcs]; + Arrays.fill(this.nodesPerDc, nodesPerDc); + } + + private static int total(int... num) + { + int tmp = 0; + for (int i : num) + tmp += i; + return tmp; + } + + public int dcs() + { + return nodesPerDc.length; + } + + public KeyspaceParams asKeyspaceParams() + { + if (this.keyspaceParams == null) + this.keyspaceParams = toKeyspaceParams(lookup); + return this.keyspaceParams; + } + + + public Map asMap() + { + if (this.map == null) + this.map = toMap(lookup); + return this.map; + } + + public ReplicatedRanges replicate(Range[] ranges, List nodes) + { + return replicate(ranges, nodes, asMap()); + } + + private static > void assertStrictlySorted(Collection coll) + { + if (coll.size() <= 1) return; + + Iterator iter = coll.iterator(); + T prev = iter.next(); + while (iter.hasNext()) + { + T next = iter.next(); + assert next.compareTo(prev) > 0 : String.format("Collection does not seem to be sorted. %s and %s are in wrong order", prev, next); + prev = next; + } + } + public static ReplicatedRanges replicate(Range[] ranges, List nodes, Map rfs) + { + assertStrictlySorted(nodes); + Map template = new HashMap<>(); + + Map> nodesByDC = nodesByDC(nodes); + Map> racksByDC = racksByDC(nodes); + + for (Map.Entry entry : rfs.entrySet()) + { + String dc = entry.getKey(); + int rf = entry.getValue(); + List nodesInThisDC = nodesByDC.get(dc); + Set racksInThisDC = racksByDC.get(dc); + int nodeCount = nodesInThisDC == null ? 0 : nodesInThisDC.size(); + int rackCount = racksInThisDC == null ? 0 : racksInThisDC.size(); + if (rf <= 0 || nodeCount == 0) + continue; + + template.put(dc, new DatacenterNodes(rf, rackCount, nodeCount)); + } + + NavigableMap>> replication = new TreeMap<>(); + + for (Range range : ranges) + { + final int idx = primaryReplica(nodes, range); + int cnt = 0; + if (idx >= 0) + { + int dcsToFill = template.size(); + + Map nodesInDCs = new HashMap<>(); + for (Map.Entry e : template.entrySet()) + nodesInDCs.put(e.getKey(), e.getValue().copy()); + + while (dcsToFill > 0 && cnt < nodes.size()) + { + Node node = nodes.get((idx + cnt) % nodes.size()); + DatacenterNodes dcNodes = nodesInDCs.get(node.dc()); + if (dcNodes != null && dcNodes.addAndCheckIfDone(node, new Location(node.dc(), node.rack()))) + dcsToFill--; + + cnt++; + } + + replication.put(range, mapValues(nodesInDCs, v -> v.nodes)); + } + else + { + // if the range end is larger than the highest assigned token, then treat it + // as part of the wraparound and replicate it to the same nodes as the first + // range. This is most likely caused by a decommission removing the node with + // the largest token. + replication.put(range, replication.get(ranges[0])); + } + } + + return combine(replication); + } + + private KeyspaceParams toKeyspaceParams(Lookup lookup) + { + Object[] args = new Object[nodesPerDc.length * 2]; + for (int i = 0; i < nodesPerDc.length; i++) + { + args[i * 2] = lookup.dc(i + 1); + args[i * 2 + 1] = nodesPerDc[i]; + } + return KeyspaceParams.nts(args); + } + + private Map toMap(Lookup lookup) + { + Map map = new TreeMap<>(); + for (int i = 0; i < nodesPerDc.length; i++) + { + map.put(lookup.dc(i + 1), nodesPerDc[i]); + } + return map; + } + + public String toString() + { + return "NtsReplicationFactor{" + + "map=" + asMap() + + '}'; + } + } + + public static class SimpleReplicationFactor extends ReplicationFactor + { + private final Lookup lookup = new DefaultLookup(); + public SimpleReplicationFactor(int total) + { + super(total); + } + + public int dcs() + { + return 1; + } + + public KeyspaceParams asKeyspaceParams() + { + return KeyspaceParams.simple(total()); + } + + public Map asMap() + { + return Collections.singletonMap(lookup.dc(1), total()); + } + + public ReplicatedRanges replicate(Range[] ranges, List nodes) + { + return replicate(ranges, nodes, total()); + } + + public static ReplicatedRanges replicate(Range[] ranges, List nodes, int rf) + { + NavigableMap> replication = new TreeMap<>(); + for (Range range : ranges) + { + Set names = new HashSet<>(); + List replicas = new ArrayList<>(); + int idx = primaryReplica(nodes, range); + if (idx >= 0) + { + for (int i = idx; i < nodes.size() && replicas.size() < rf; i++) + addIfUnique(replicas, names, nodes.get(i)); + + for (int i = 0; replicas.size() < rf && i < idx; i++) + addIfUnique(replicas, names, nodes.get(i)); + if (range.start == Long.MIN_VALUE) + replication.put(ranges[ranges.length - 1], replicas); + replication.put(range, replicas); + } + else + { + // if the range end is larger than the highest assigned token, then treat it + // as part of the wraparound and replicate it to the same nodes as the first + // range. This is most likely caused by a decommission removing the node with + // the largest token. + replication.put(range, replication.get(ranges[0])); + } + } + + return new ReplicatedRanges(ranges, Collections.unmodifiableNavigableMap(replication)); + } + + public String toString() + { + return "SimpleReplicationFactor{" + + "rf=" + total() + + '}'; + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java new file mode 100644 index 0000000000..babe38a9c1 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java @@ -0,0 +1,379 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.PrimitiveIterator; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.junit.Test; + +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.*; +import static org.junit.Assert.assertTrue; + +public class PlacementSimulatorTest +{ + @Test + public void testMove() + { + testMove(100, 200, 300, 400, 350, new SimpleReplicationFactor(3)); + + Random rng = new Random(); + for (int i = 0; i < 1000; i++) + { + PrimitiveIterator.OfInt ints = rng.ints(5).distinct().iterator(); + testMove(ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), new SimpleReplicationFactor(3)); + } + } + + public void testMove(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf) + { + NodeFactory factory = PlacementSimulator.nodeFactory(); + Node movingNode = factory.make(1, 1, 1).overrideToken(t1); + List orig = Arrays.asList(movingNode, + factory.make(2, 1, 1).overrideToken(t2), + factory.make(3, 1, 1).overrideToken(t3), + factory.make(4, 1, 1).overrideToken(t4)); + orig.sort(Node::compareTo); + + SimulatedPlacements placements = new SimulatedPlacements(rf, + orig, + rf.replicate(orig).asMap(), + rf.replicate(orig).asMap(), + Collections.emptyList()); + Transformations steps = move(placements, movingNode, newToken); + + List afterSplit = split(orig, newToken); + List finalState = moveFinalState(orig, movingNode, newToken); + + placements = steps.advance(placements); + placements = steps.advance(placements); + + assertPlacements(placements, + rf.replicate(afterSplit).asMap(), + superset(rf.replicate(afterSplit).asMap(), + rf.replicate(split(finalState, movingNode.token())).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(split(finalState, movingNode.token())).asMap(), + superset(rf.replicate(afterSplit).asMap(), + rf.replicate(split(finalState, movingNode.token())).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(finalState).asMap(), + rf.replicate(finalState).asMap()); + } + + @Test + public void testBootstrap() + { + testBootstrap(100, 200, 300, 400, 350, new SimpleReplicationFactor(3)); + + Random rng = new Random(); + for (int i = 0; i < 1000; i++) + { + PrimitiveIterator.OfInt ints = rng.ints(5).distinct().iterator(); + testBootstrap(ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), new SimpleReplicationFactor(3)); + } + } + + public void testBootstrap(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf) + { + NodeFactory factory = PlacementSimulator.nodeFactory(); + List orig = Arrays.asList(factory.make(1, 1, 1).overrideToken(t1), + factory.make(2, 1, 1).overrideToken(t2), + factory.make(3, 1, 1).overrideToken(t3), + factory.make(4, 1, 1).overrideToken(t4)); + orig.sort(Node::compareTo); + + Node newNode = factory.make(5, 1, 1).overrideToken(newToken); + SimulatedPlacements placements = new SimulatedPlacements(rf, + orig, + rf.replicate(orig).asMap(), + rf.replicate(orig).asMap(), + Collections.emptyList()); + Transformations steps = join(placements, newNode); + + List afterSplit = split(orig, newToken); + List finalState = bootstrapFinalState(orig, newNode, newToken); + + placements = steps.advance(placements); + placements = steps.advance(placements); + + assertPlacements(placements, + rf.replicate(afterSplit).asMap(), + superset(rf.replicate(afterSplit).asMap(), + rf.replicate(finalState).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(finalState).asMap(), + superset(rf.replicate(afterSplit).asMap(), + rf.replicate(finalState).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(finalState).asMap(), + rf.replicate(finalState).asMap()); + } + + + @Test + public void testDecommission() + { + testDecommission(100, 200, 300, 400, 350, new SimpleReplicationFactor(3)); + + Random rng = new Random(); + for (int i = 0; i < 1000; i++) + { + PrimitiveIterator.OfInt ints = rng.ints(5).distinct().iterator(); + testDecommission(ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), ints.nextInt(), new SimpleReplicationFactor(3)); + } + } + + public void testDecommission(long t1, long t2, long t3, long t4, long t5, ReplicationFactor rf) + { + NodeFactory factory = PlacementSimulator.nodeFactory(); + Node leavingNode = factory.make(1, 1, 1).overrideToken(t1); + List orig = Arrays.asList(leavingNode, + factory.make(2, 1, 1).overrideToken(t2), + factory.make(3, 1, 1).overrideToken(t3), + factory.make(4, 1, 1).overrideToken(t4), + factory.make(4, 1, 1).overrideToken(t5)); + orig.sort(Node::compareTo); + + SimulatedPlacements placements = new SimulatedPlacements(rf, + orig, + rf.replicate(orig).asMap(), + rf.replicate(orig).asMap(), + Collections.emptyList()); + Transformations steps = leave(placements, leavingNode); + + List finalState = leaveFinalState(orig, leavingNode.token()); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(orig).asMap(), + superset(rf.replicate(orig).asMap(), + rf.replicate(split(finalState, leavingNode.token())).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(split(finalState, leavingNode.token())).asMap(), + superset(rf.replicate(orig).asMap(), + rf.replicate(split(finalState, leavingNode.token())).asMap())); + + placements = steps.advance(placements); + assertPlacements(placements, + rf.replicate(finalState).asMap(), + rf.replicate(finalState).asMap()); + } + + public static List moveFinalState(List nodes, Node target, long newToken) + { + nodes = filter(nodes, n -> n.idx() != target.idx()); // filter out current owner + nodes = split(nodes, newToken); // materialize new token + nodes = move(nodes, newToken, target); // move new token to the node + return nodes; + } + + public static List bootstrapFinalState(List nodes, Node newNode, long newToken) + { + nodes = split(nodes, newToken); // materialize new token + nodes = move(nodes, newToken, newNode); // move new token to the node + return nodes; + } + + public static List leaveFinalState(List nodes, long leavingToken) + { + nodes = filter(nodes, n -> n.token() != leavingToken); + return nodes; + } + + @Test + public void simulate() throws Throwable + { + for (int rf : new int[]{ 2, 3, 5 }) + { + simulate(new SimpleReplicationFactor(rf)); + } + } + + public void simulate(ReplicationFactor rf) throws Throwable + { + NodeFactory factory = PlacementSimulator.nodeFactory(); + List orig = Collections.singletonList(factory.make(1, 1, 1)); + + ModelChecker modelChecker = new ModelChecker<>(); + AtomicInteger addressCounter = new AtomicInteger(1); + AtomicInteger operationCounter = new AtomicInteger(1); + + modelChecker.init(new SimulatedPlacements(rf, + orig, + rf.replicate(orig).asMap(), + rf.replicate(orig).asMap(), + Collections.emptyList()), + new SUTState()) + .step((state, sut) -> state.nodes.size() < rf.total(), + (state, sut, rng) -> new ModelChecker.Pair<>(PlacementSimulator.joinFully(state, factory.make(addressCounter.incrementAndGet(), 1, 1)), + sut)) + .step((state, sut) -> state.nodes.size() >= rf.total() && state.stashedStates.size() < 1, + (state, sut, rng) -> { + if (operationCounter.getAndIncrement() % rf.total() == 1) + { + // randomly schedule either decommission or replacement of an existing node + Node toRemove = state.nodes.get(rng.nextInt(0, state.nodes.size() - 1)); + state = state.withStashed(rng.nextBoolean() + ? replace(state, toRemove, factory.make(addressCounter.incrementAndGet(), 1, 1).overrideToken(toRemove.token())) + : leave(state, toRemove)); + return new ModelChecker.Pair<>(state, sut); + } + else + { + // schedule bootstrapping an additional node + return new ModelChecker.Pair<>(state.withStashed(join(state, + factory.make(addressCounter.incrementAndGet(), 1, 1))), + sut); + } + }) + .step((state, sut) -> !state.stashedStates.isEmpty(), + (state, sut, rng) -> { + int idx = rng.nextInt(0, state.stashedStates.size() - 1); + state = state.stashedStates.get(idx).advance(state); + return new ModelChecker.Pair<>(state, sut); + }) + .exitCondition((state, sut) -> { + if (addressCounter.get() >= 100 && state.stashedStates.isEmpty()) + { + // After all commands are done, we should arrive to correct placements + assertRanges(state.writePlacements, + rf.replicate(state.nodes).asMap()); + assertRanges(state.readPlacements, + rf.replicate(state.nodes).asMap()); + return true; + } + return false; + }) + .run(); + } + + @Test + public void revertPartialBootstrap() throws Throwable + { + for (int n : new int[]{ 2, 3, 5 }) + { + ReplicationFactor rf = new SimpleReplicationFactor(n); + NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable(); + List nodes = new ArrayList<>(10); + for (int i = 1; i <= 10; i++) + nodes.add(factory.make(i, 1, 1)); + + SimulatedPlacements sim = new SimulatedPlacements(rf, nodes, rf.replicate(nodes).asMap(), rf.replicate(nodes).asMap(), Collections.emptyList()); + Node newNode = factory.make(11, 1, 1); + revertPartiallyCompleteOp(sim, () -> join(sim, newNode), 3); + } + } + + @Test + public void revertPartialLeave() + { + for (int n : new int[]{ 2, 3, 5 }) + { + ReplicationFactor rf = new SimpleReplicationFactor(n); + NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable(); + List nodes = new ArrayList<>(10); + for (int i = 1; i <= 10; i++) + nodes.add(factory.make(i, 1, 1)); + + Node toRemove = nodes.get(5); + SimulatedPlacements sim = new SimulatedPlacements(rf, nodes, rf.replicate(nodes).asMap(), rf.replicate(nodes).asMap(), Collections.emptyList()); + revertPartiallyCompleteOp(sim, () -> leave(sim, toRemove), 2); + } + } + + @Test + public void revertPartialReplacement() + { + for (int n : new int[]{ 2, 3, 5 }) + { + ReplicationFactor rf = new SimpleReplicationFactor(n); + NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable(); + List nodes = new ArrayList<>(10); + for (int i = 1; i <= 10; i++) + nodes.add(factory.make(i, 1, 1)); + + Node toReplace = nodes.get(5); + SimulatedPlacements sim = new SimulatedPlacements(rf, + nodes, + rf.replicate(nodes).asMap(), + rf.replicate(nodes).asMap(), + Collections.emptyList()); + Node replacement = factory.make(11, 1, 1).overrideToken(toReplace.token()); + revertPartiallyCompleteOp(sim, () -> replace(sim, toReplace, replacement), 2); + } + } + + + private void revertPartiallyCompleteOp(SimulatedPlacements startingState, + Supplier opProvider, + int maxStepsBeforeRevert) + { + // reverting the bootstrap after only n steps have been executed + // for the various operations steps that may be performed before revert are: + // bootstrap_diffBased: [split, start, mid] + // bootstrap_explicitPlacement: [split, start, mid] + // replace_directly: [start, mid] + // leave_diffBased: [start, mid] + + for (int i = 1; i <= maxStepsBeforeRevert; i++) + startThenRevertOp(startingState, opProvider, i); + } + + private void startThenRevertOp(SimulatedPlacements sim, + Supplier opProvider, + int stepsToExecute) + { + Map> startingReadPlacements = sim.readPlacements; + Map> startingWritePlacements = sim.writePlacements; + Transformations steps = opProvider.get(); + sim = sim.withStashed(steps); + // execute the required steps + for (int i = 0; i < stepsToExecute; i++) + sim = steps.advance(sim); + + // now revert them + sim = steps.revertPublishedEffects(sim); + + assertRanges(startingReadPlacements, sim.readPlacements); + assertRanges(startingWritePlacements, sim.writePlacements); + assertTrue(sim.stashedStates.isEmpty()); + } + + public static class SUTState + { + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/QuorumIntersectionSimulatorTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/QuorumIntersectionSimulatorTest.java new file mode 100644 index 0000000000..cc6723a6ff --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/QuorumIntersectionSimulatorTest.java @@ -0,0 +1,142 @@ +/* + * 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.log; + +import java.util.*; + +import org.junit.Test; + +public class QuorumIntersectionSimulatorTest +{ + public static class PermutationIterator implements Iterator> { + + private final List arr; + private final int[] pointers; + private final int variants; + + public PermutationIterator(List arr, int size) + { + this.arr = arr; + this.pointers = new int[size]; + this.variants = arr.size() - 1; + } + + public boolean hasNext() + { + for (int p : pointers) + { + if (p < variants) + return true; + } + return false; + } + + public List next() + { + for (int i = 0; i < pointers.length; i++) + { + if (pointers[i] < variants) + { + pointers[i]++; + break; + } + else + { + for (int j = 0; j <= i; j++) + pointers[i] = 0; + } + } + + List res = new ArrayList<>(); + for (int pointer : pointers) + res.add(arr.get(pointer)); + + return res; + } + } + + @Test + public void checkQuorumIntersections() + { + View view1 = new View(set("a", "b", "c"), + set("a", "b", "c", "d")); + + View view2 = new View(set("b", "c", "d"), + set("a", "b", "c", "d")); + + checkQuorums(view1, view2); + + View view3 = new View(set("b", "c", "d"), + set("b", "c", "d")); + + checkQuorums(view2, view3); + } + + public void checkQuorums(View readingNode, View writingNode) + { + Iterator> readQuorums = new PermutationIterator<>(readingNode.r, readingNode.r.size() / 2 + 1); + while (readQuorums.hasNext()) + { + List readQuorum = readQuorums.next(); + Set readQuorumSet = new HashSet<>(readQuorum); + if (readQuorum.size() != readQuorumSet.size()) + continue; + + Iterator> writeQuorums = new PermutationIterator<>(writingNode.w, writingNode.w.size() / 2 + 1); + while (writeQuorums.hasNext()) + { + List writeQuorum = writeQuorums.next(); + Set writeQuorumSet = new HashSet<>(writeQuorum); + if (writeQuorum.size() != writeQuorumSet.size()) + continue; + + assert hasIntersection(new HashSet<>(readQuorum), new HashSet<>(writeQuorum)) : + String.format("No intersections between quorums %s and %s", readQuorum, writeQuorum); + } + } + } + + public static boolean hasIntersection(Set l, Set r) { + for (T l1 : l) + { + for (T r1 : r) + { + if (l1.equals(r1)) + return true; + } + } + return false; + } + + static class View + { + final List r; + final List w; + + View(List r, List w) + { + this.r = r; + this.w = w; + } + } + + public List set(String... nodes) { + return Arrays.asList(nodes); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ReconfigureCMSTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ReconfigureCMSTest.java new file mode 100644 index 0000000000..38a595b5af --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ReconfigureCMSTest.java @@ -0,0 +1,141 @@ +/* + * 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.log; + +import java.util.Random; +import java.util.function.Supplier; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.EntireRange; +import org.apache.cassandra.tcm.sequences.CancelCMSReconfiguration; +import org.apache.cassandra.tcm.sequences.ProgressBarrier; +import org.apache.cassandra.tcm.sequences.ReconfigureCMS; +import org.apache.cassandra.tcm.transformations.cms.PrepareCMSReconfiguration; +import org.apache.cassandra.utils.FBUtilities; + +public class ReconfigureCMSTest extends FuzzTestBase +{ + @Test + public void expandAndShrinkCMSTest() throws Throwable + { + try (Cluster cluster = Cluster.build(6) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + cluster.setUncaughtExceptionsFilter(t -> t.getMessage() != null && t.getMessage().contains("There are not enough nodes in dc0 datacenter to satisfy replication factor")); + Random rnd = new Random(2); + Supplier nodeSelector = () -> rnd.nextInt(cluster.size() - 1) + 1; + cluster.get(nodeSelector.get()).nodetoolResult("reconfigurecms", "--sync", "0").asserts().failure(); + cluster.get(nodeSelector.get()).nodetoolResult("reconfigurecms", "--sync", "500").asserts().failure(); + cluster.get(nodeSelector.get()).nodetoolResult("reconfigurecms", "--sync", "5").asserts().success(); + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + Assert.assertEquals(5, metadata.fullCMSMembers().size()); + Assert.assertEquals(ReplicationParams.simpleMeta(5, metadata.directory.knownDatacenters()), + metadata.placements.keys().stream().filter(ReplicationParams::isMeta).findFirst().get()); + }); + cluster.stream().forEach(i -> { + Assert.assertTrue(i.executeInternal(String.format("SELECT * FROM %s.%s", SchemaConstants.METADATA_KEYSPACE_NAME, DistributedMetadataLogKeyspace.TABLE_NAME)).length > 0); + }); + + cluster.get(nodeSelector.get()).nodetoolResult("reconfigurecms", "--sync", "1").asserts().success(); + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + Assert.assertEquals(1, metadata.fullCMSMembers().size()); + Assert.assertEquals(ReplicationParams.simpleMeta(1, metadata.directory.knownDatacenters()), + metadata.placements.keys().stream().filter(ReplicationParams::isMeta).findFirst().get()); + }); + } + } + + @Test + public void cancelCMSReconfigurationTest() throws Throwable + { + try (Cluster cluster = Cluster.build(4) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .set("progress_barrier_default_consistency_level", ConsistencyLevel.ALL) + .with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + cluster.get(1).nodetoolResult("reconfigurecms", "--sync", "2").asserts().success(); + cluster.get(1).runOnInstance(() -> { + ClusterMetadataService.instance().commit(new PrepareCMSReconfiguration.Complex(ReplicationParams.simple(3).asMeta())); + ReconfigureCMS reconfigureCMS = (ReconfigureCMS) ClusterMetadata.current().inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + ClusterMetadataService.instance().commit(reconfigureCMS.next); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + try + { + ClusterMetadataService.instance().commit(reconfigureCMS.next); + Assert.fail("Should not be possible to commit same `advance` twice"); + } + catch (Throwable t) + { + Assert.assertTrue(t.getMessage().contains("This transformation (0) has already been applied")); + } + reconfigureCMS = (ReconfigureCMS) ClusterMetadata.current().inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + Assert.assertNotNull(reconfigureCMS.next.activeTransition); + + ClusterMetadataService.instance().commit(CancelCMSReconfiguration.instance); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + ClusterMetadata metadata = ClusterMetadata.current(); + Assert.assertNull(metadata.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance)); + Assert.assertEquals(2, metadata.fullCMSMembers().size()); + DataPlacement placements = metadata.placements.get(ReplicationParams.meta(metadata)); + Assert.assertEquals(placements.reads, placements.writes); + }); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadataService.instance().commit(new PrepareCMSReconfiguration.Complex(ReplicationParams.simple(4).asMeta())); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + + ReconfigureCMS reconfigureCMS = (ReconfigureCMS) ClusterMetadata.current().inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + ClusterMetadataService.instance().commit(reconfigureCMS.next); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + reconfigureCMS = (ReconfigureCMS) ClusterMetadata.current().inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + ClusterMetadataService.instance().commit(reconfigureCMS.next); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + reconfigureCMS = (ReconfigureCMS) ClusterMetadata.current().inProgressSequences.get(ReconfigureCMS.SequenceKey.instance); + Assert.assertNull(reconfigureCMS.next.activeTransition); + + ClusterMetadataService.instance().commit(CancelCMSReconfiguration.instance); + ProgressBarrier.propagateLast(EntireRange.affectedRanges(ClusterMetadata.current())); + ClusterMetadata metadata = ClusterMetadata.current(); + Assert.assertNull(metadata.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance)); + Assert.assertTrue(metadata.fullCMSMembers().contains(FBUtilities.getBroadcastAddressAndPort())); + Assert.assertEquals(3, metadata.fullCMSMembers().size()); + DataPlacement placements = metadata.placements.get(ReplicationParams.meta(metadata)); + Assert.assertEquals(placements.reads, placements.writes); + }); + } + }} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/RegisterTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/RegisterTest.java new file mode 100644 index 0000000000..8cfa95da27 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/RegisterTest.java @@ -0,0 +1,169 @@ +/* + * 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.log; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.UnknownHostException; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.SealPeriod; +import org.apache.cassandra.tcm.transformations.Unregister; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES; + +public class RegisterTest extends TestBaseImpl +{ + @Test + public void testRegistrationIdempotence() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(5)) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .createWithoutStarting()) + { + // Make sure 2 and 3 do not race for ID + for (int i : new int[]{ 1,3,2 }) + cluster.get(i).startup(); + + for (int i : new int[]{ 3, 2 }) + { + cluster.get(i).runOnInstance(() -> { + ClusterMetadataService.instance().commit(new PrepareLeave(ClusterMetadata.current().myNodeId(), + true, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + UnbootstrapAndLeave unbootstrapAndLeave = (UnbootstrapAndLeave) ClusterMetadata.current().inProgressSequences.get(ClusterMetadata.current().myNodeId()); + ClusterMetadataService.instance().commit(unbootstrapAndLeave.startLeave); + ClusterMetadataService.instance().commit(unbootstrapAndLeave.midLeave); + ClusterMetadataService.instance().commit(unbootstrapAndLeave.finishLeave); + ClusterMetadataService.instance().commit(new Unregister(ClusterMetadata.current().myNodeId())); + }); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadataService.instance().commit(SealPeriod.instance); + }); + + IInstanceConfig config = cluster.newInstanceConfig(); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(); + } + } + } + + @Test + public void serializationVersionDisagreementTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(2) + .createWithoutStarting(); + WithProperties prop = new WithProperties().set(TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES, "true")) + { + cluster.get(1).startup(); + cluster.get(1).runOnInstance(() -> { + try + { + // Register a ghost node with V0 to fake-force V0 serialization. In a real world cluster we will always be upgrading from a smaller version. + ClusterMetadataService.instance().commit(new Register(new NodeAddresses(InetAddressAndPort.getByName("127.0.0.10")), + ClusterMetadata.current().directory.location(ClusterMetadata.current().myNodeId()), + new NodeVersion(NodeVersion.CURRENT.cassandraVersion, Version.V0))); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + ClusterMetadataService.instance().commit(SealPeriod.instance); + }); + + cluster.get(2).runOnInstance(() -> { + try + { + Field field = NodeVersion.class.getDeclaredField("CURRENT"); + Field modifiers = Field.class.getDeclaredField("modifiers"); + + field.setAccessible(true); + modifiers.setAccessible(true); + + int newModifiers = field.getModifiers() & ~Modifier.FINAL; + modifiers.setInt(field, newModifiers); + field.set(null, new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), NodeVersion.CURRENT_METADATA_VERSION)); + + } + catch (NoSuchFieldException | IllegalAccessException e) + { + throw new RuntimeException(e); + } + }); + + cluster.get(2).startup(); + } + } + + @Test + public void replayLocallyFromV0Snapshot() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .createWithoutStarting()) + { + cluster.get(1).startup(); + cluster.get(1).runOnInstance(() -> { + try + { + // Register a ghost node with V0 to fake-force V0 serialization. In a real world cluster we will always be upgrading from a smaller version. + ClusterMetadataService.instance().commit(new Register(new NodeAddresses(InetAddressAndPort.getByName("127.0.0.10")), + ClusterMetadata.current().directory.location(ClusterMetadata.current().myNodeId()), + new NodeVersion(NodeVersion.CURRENT.cassandraVersion, Version.V0))); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + ClusterMetadataService.instance().commit(SealPeriod.instance); + + ClusterMetadata cm = new MetadataSnapshots.SystemKeyspaceMetadataSnapshots().getSnapshot(ClusterMetadata.current().epoch); + cm.equals(ClusterMetadata.current()); + }); + + + } + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/RequestCurrentEpochTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/RequestCurrentEpochTest.java new file mode 100644 index 0000000000..43c318c114 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/RequestCurrentEpochTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test.log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.CustomTransformation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RequestCurrentEpochTest extends FuzzTestBase +{ + @Test + public void testRequestingPeerWatermarks() throws Throwable + { + try (Cluster cluster = builder().withNodes(3).start()) + { + init(cluster); + IInvokableInstance cmsNode = cluster.get(1); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsNode); + assertEpochs(cluster); + cluster.schemaChange(withKeyspace("create table %s.t1 (id int primary key, x int)")); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsNode); + assertEpochs(cluster); + // This is very bad and wouldn't possible on a real node, but we just + // want to affect the epoch on one node so we can assert that it reports + // it properly when asked. A better way would be to block replication and + // replay messages to that node, but that's a bit more work. + IInvokableInstance inst = cluster.get(3); + Epoch newEpoch = ClusterUtils.getNextEpoch(inst); + inst.runOnInstance(() -> { + ClusterMetadataService.instance() + .log() + .append(new Entry(Entry.Id.NONE, + newEpoch, + CustomTransformation.make("DANGER"))); + try + { + ClusterMetadataService.instance().awaitAtLeast(newEpoch); + } + catch (InterruptedException | TimeoutException e) + { + throw new RuntimeException(e); + } + }); + Map canonicalEpochs = getEpochsDirectly(cluster); + assertTrue(canonicalEpochs.get(inst.broadcastAddress().toString()) + .isAfter(canonicalEpochs.get(cmsNode.broadcastAddress().toString()))); + assertEpochs(cluster); + } + } + + private void assertEpochs(Cluster cluster) + { + assertEpochs(cluster, getEpochsDirectly(cluster)); + } + + private void assertEpochs(Cluster cluster, Map canonicalEpochs) + { + cluster.forEach(inst -> assertEquals(canonicalEpochs, getEpochsIndirectly(inst))); + } + + private Map getEpochsDirectly(Cluster cluster) + { + Map epochs = new HashMap<>(); + cluster.forEach(inst -> epochs.put(inst.broadcastAddress().toString(), ClusterUtils.getCurrentEpoch(inst))); + return epochs; + } + + private Map getEpochsIndirectly(IInvokableInstance requester) + { + Map epochs = ClusterUtils.getPeerEpochs(requester); + return epochs; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java new file mode 100644 index 0000000000..2c52ea20f1 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java @@ -0,0 +1,150 @@ +/* + * 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.log; + +import java.io.IOException; +import java.util.concurrent.Callable; + +import org.junit.Assert; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.model.sut.SystemUnderTest; +import harry.visitors.GeneratingVisitor; +import harry.visitors.MutatingRowVisitor; +import harry.visitors.Visitor; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.*; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJVMTokenAwareVisitorExecutor; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getClusterMetadataVersion; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; + +public class ResumableStartupTest extends FuzzTestBase +{ + private static int WRITES = 2000; + + @Test + public void bootstrapWithDeferredJoinTest() throws Throwable + { + Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build()); + + + try (Cluster cluster = builder().withNodes(1) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .createWithoutStarting()) + { + IInvokableInstance cmsInstance = cluster.get(1); + configBuilder.setSUT(() -> new InJvmSut(cluster)); + Run run = configBuilder.build().createRun(); + + cmsInstance.config().set("auto_bootstrap", true); + cmsInstance.startup(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + + Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.NODE_LOCAL)); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance newInstance = cluster.bootstrap(config); + + withProperty(CassandraRelevantProperties.TEST_WRITE_SURVEY, true, newInstance::startup); + + // Write with ONE, replicate via pending range mechanism + visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.ONE)); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + Epoch currentEpoch = getClusterMetadataVersion(cmsInstance); + // Quick check that schema changes are possible with nodes in write survey mode (i.e. with ranges locked) + cluster.coordinator(1).execute("ALTER TABLE " + run.schemaSpec.keyspace + "." + run.schemaSpec.table + + " WITH comment = 'Schema alterations which do not affect placements should" + + " not be restricted by in flight operations';", + ConsistencyLevel.ALL); + + final String newAddress = ClusterUtils.getBroadcastAddressHostWithPortString(newInstance); + final String keyspace = run.schemaSpec.keyspace; + boolean newReplicaInCorrectState = cluster.get(1).callOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ksm = metadata.schema.getKeyspaceMetadata(keyspace); + boolean isWriteReplica = false; + boolean isReadReplica = false; + for (InetAddressAndPort readReplica : metadata.placements.get(ksm.params.replication).reads.byEndpoint().keySet()) + { + if (readReplica.getHostAddressAndPort().equals(newAddress)) + isReadReplica = true; + } + for (InetAddressAndPort writeReplica : metadata.placements.get(ksm.params.replication).writes.byEndpoint().keySet()) + { + if (writeReplica.getHostAddressAndPort().equals(newAddress)) + isWriteReplica = true; + } + return (isWriteReplica && !isReadReplica); + }); + Assert.assertTrue("Expected new instance to be a write replica only", newReplicaInCorrectState); + + Callable finishedBootstrap = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + newInstance.runOnInstance(() -> { + try + { + StorageService.instance.joinRing(); + } + catch (IOException e) + { + throw new RuntimeException("Error joining ring", e); + } + }); + Epoch next = finishedBootstrap.call(); + Assert.assertEquals(String.format("Expected epoch after schema change, mid join & finish join to be %s, but was %s", + next.getEpoch(), currentEpoch.getEpoch() + 3), + next.getEpoch(), currentEpoch.getEpoch() + 3); + + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run); + model.validateAll(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/RngUtils.java b/test/distributed/org/apache/cassandra/distributed/test/log/RngUtils.java new file mode 100644 index 0000000000..d2cf69bd3e --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/RngUtils.java @@ -0,0 +1,106 @@ +/* + * 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.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//TODO: this is borrowed from Harry, temporarily +public class RngUtils +{ + private static final Logger logger = LoggerFactory.getLogger(RngUtils.class); + + private static final long CONSTANT = 0x2545F4914F6CDD1DL; + public static long next(long input) + { + if (input == 0) + return next(CONSTANT); + + return xorshift64star(input); + } + + public static long xorshift64star(long input) + { + input ^= input >> 12; + input ^= input << 25; // b + input ^= input >> 27; // c + return input * CONSTANT; + } + + public static long[] next(long current, int n) + { + long[] next = new long[n]; + for (int i = 0; i < n; i++) + { + current = next(current); + next[i] = current; + } + return next; + } + + public static byte[] asBytes(long current) + { + byte[] bytes = new byte[Long.BYTES]; + for (int i = 0; i < Long.BYTES; i++) + { + bytes[i] = (byte) (current & 0xFF); + current >>= current; + } + return bytes; + } + + public static byte asByte(long current) + { + return (byte) current; + } + + public static int asInt(long current) + { + return (int) current; + } + + // TODO: this needs some improvement + public static int asInt(long current, int max) + { + return Math.abs((int) current % max); + } + + // Generate a value in [min, max] range: from min _inclusive_ to max _inclusive_. + public static int asInt(long current, int min, int max) + { + if (min == max) + return min; + return min + asInt(current, max - min); + } + + public static boolean asBoolean(long current) + { + return (current & 1) == 1; + } + + public static float asFloat(long current) + { + return Float.intBitsToFloat((int) current); + } + + public static double asDouble(long current) + { + return Double.longBitsToDouble(current); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java new file mode 100644 index 0000000000..57ca794882 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java @@ -0,0 +1,562 @@ +/* + * 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.log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterators; + +import org.junit.Assert; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.transformations.CancelInProgressSequence; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareMove; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; + +import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken; +import static org.apache.cassandra.distributed.test.log.CMSTestBase.CMSSut; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements; +import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations; + +public abstract class SimulatedOperation +{ + enum Type{JOIN, LEAVE, REPLACE, MOVE} + enum Status{READY, STARTED} + + public final Type type; + public final Node[] nodes; + public final Status status; + public Iterator sutActions; + public Transformations simulatedActions; + + public static ModelState join(CMSSut sut, ModelState state, Node node) + { + ModelState.Transformer transformer = state.transformer(); + new Join(node).create(sut, state.simulatedPlacements, transformer); + return transformer.transform(); + } + + public static ModelState joinWithoutBootstrap(ModelState state, + CMSSut sut, + Node node) + { + sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider())); + SimulatedPlacements simulatedState = state.simulatedPlacements; + if (simulatedState == null) + { + List nodes = Collections.singletonList(node); + simulatedState = new SimulatedPlacements(sut.rf, + nodes, + sut.rf.replicate(nodes).asMap(), + sut.rf.replicate(nodes).asMap(), + Collections.emptyList()); + } + else + { + Transformations simulatedActions = PlacementSimulator.join(simulatedState, node); + while (simulatedActions.hasNext()) + simulatedState = simulatedActions.advance(simulatedState); + } + return state.transformer() + .withJoined(node) + .updateSimulation(simulatedState) + .transform(); + + } + + public static ModelState leave(CMSSut sut, ModelState state, Node node) + { + ModelState.Transformer transformer = state.transformer(); + new Leave(node).create(sut, state.simulatedPlacements, transformer); + return transformer.transform(); + } + + public static ModelState replace(CMSSut sut, ModelState state, Node toReplace, Node replacement) + { + ModelState.Transformer transformer = state.transformer(); + new Replace(toReplace, replacement).create(sut, state.simulatedPlacements, transformer); + return transformer.transform(); + } + + public static ModelState move(CMSSut sut, ModelState state, Node movingNode, Node newNode) + { + ModelState.Transformer transformer = state.transformer(); + new Move(movingNode, newNode).create(sut, state.simulatedPlacements, transformer); + return transformer.transform(); + } + + public SimulatedOperation(Type type, + Node[] nodes, + Iterator sutActions, + Status status, + Transformations simulatedActions) + { + this.type = type; + this.nodes = nodes; + this.sutActions = sutActions; + this.status = status; + this.simulatedActions = simulatedActions; + } + + public abstract void create(CMSSut cmsSut, SimulatedPlacements simulatedPlacements, ModelState.Transformer transformer); + + protected abstract void finishOperation(ModelState.Transformer transformer); + + protected abstract void incrementCancel(ModelState.Transformer transformer); + + public ModelState advance(ModelState simulatedState) + { + ModelState.Transformer transformer = simulatedState.transformer(); + advance(simulatedState.simulatedPlacements, transformer); + return transformer.transform(); + } + + public void advance(SimulatedPlacements simulatedState, ModelState.Transformer transformer) + { + ClusterMetadata m1 = ClusterMetadata.current(); + + sutActions.next(); + ClusterMetadata m2 = ClusterMetadata.current(); + + Map, VersionedEndpoints.ForRange> after = m2.placements.get(simulatedState.rf.asKeyspaceParams().replication).reads.replicaGroups(); + m1.placements.get(simulatedState.rf.asKeyspaceParams().replication).reads.replicaGroups().forEach((k, beforePlacements) -> { + if (after.containsKey(k)) + { + VersionedEndpoints.ForRange afterPlacements = after.get(k); + if (!beforePlacements.get().stream().collect(Collectors.toSet()).equals(after.get(k).get().stream().collect(Collectors.toSet()))) + { + Assert.assertTrue(String.format("Expected the range %s to bump epoch from %s, but it was %s, because the endpoints have changed:\n%s\n%s", + k, beforePlacements.lastModified(), afterPlacements.lastModified(), + beforePlacements.get(), afterPlacements.get()), + afterPlacements.lastModified().isAfter(beforePlacements.lastModified())); + } + else + { + Assert.assertTrue(String.format("Expected the range %s to have the same epoch (%s), but it was %s.", + k, beforePlacements.lastModified(), afterPlacements.lastModified()), + afterPlacements.lastModified().is(beforePlacements.lastModified())); + } + } + }); + + + simulatedState = simulatedActions.advance(simulatedState); + + transformer.removeOperation(this) + .updateSimulation(simulatedState); + + if (sutActions.hasNext()) + transformer.addOperation(started()); + else + finishOperation(transformer); + } + + public void cancel(CMSSut sut, SimulatedPlacements simulatedPlacements, ModelState.Transformer transformer) + { + ClusterMetadata metadata = sut.service.metadata(); + Node node = targetNode(); + MultiStepOperation operation = metadata.inProgressSequences.get(node.nodeId()); + assert operation != null : "No in-progress sequence found for node " + node.nodeId(); + sut.service.commit(new CancelInProgressSequence(node.nodeId())); + + simulatedPlacements = simulatedActions.revertPublishedEffects(simulatedPlacements); + transformer.removeOperation(this) + .updateSimulation(simulatedPlacements); + incrementCancel(transformer); + } + + protected abstract Node targetNode(); + + protected abstract SimulatedOperation started(); + + public String toString() + { + return "OperationState{" + + "type=" + type + + ", nodes=" + Arrays.toString(nodes) + + ", remaining=" + sutActions.hasNext() + + ", status=" + status + + '}'; + } + + static class Join extends SimulatedOperation + { + protected Join(Node node) + { + this(new Node[]{ node }, null, Status.READY, null); + } + + public Join(Node[] nodes, Iterator sutActions, Status status, Transformations simulatedActions) + { + super(Type.JOIN, nodes, sutActions, status, simulatedActions); + } + + @Override + public void create(CMSSut sut, SimulatedPlacements simulatedState, ModelState.Transformer transformer) + { + assert simulatedActions == null; + Node toAdd = targetNode(); + Optional maybePlan = prepareJoin(sut, toAdd); + if (!maybePlan.isPresent()) + { + transformer.incrementRejected() + .recycleRejected(toAdd); + return; + } + + BootstrapAndJoin plan = maybePlan.get(); + sutActions = toIter(sut.service, plan.startJoin, plan.midJoin, plan.finishJoin); + simulatedActions = PlacementSimulator.join(simulatedState, toAdd); + + // immediately execute the first step of bootstrap transformations, the splitting of existing ranges. This + // is so that subsequent planned operations base their transformations on the split ranges. + simulatedState = simulatedActions.advance(simulatedState.withStashed(simulatedActions)); + + transformer.addOperation(this) + .updateSimulation(simulatedState); + } + + @Override + protected void finishOperation(ModelState.Transformer transformer) + { + transformer.withJoined(targetNode()); + } + + @Override + protected void incrementCancel(ModelState.Transformer transformer) + { + transformer.incrementCancelledJoin(); + } + + protected Node targetNode() + { + return nodes[0]; + } + + @Override + public SimulatedOperation started() + { + return new Join(nodes, sutActions, Status.STARTED, simulatedActions); + } + } + + public static class Leave extends SimulatedOperation + { + protected Leave(Node node) + { + this(new Node[]{ node }, null, Status.READY, null); + } + + public Leave(Node[] nodes, Iterator sutActions, Status status, Transformations simulatedActions) + { + super(Type.LEAVE, nodes, sutActions, status, simulatedActions); + } + + @Override + public void create(CMSSut sut, SimulatedPlacements simulatedState, ModelState.Transformer transformer) + { + assert simulatedActions == null; + + Node toRemove = targetNode(); + Optional maybePlan = prepareLeave(sut, toRemove); + if (!maybePlan.isPresent()) + { + transformer.incrementRejected(); + return; + } + + UnbootstrapAndLeave plan = maybePlan.get(); + sutActions = toIter(sut.service, plan.startLeave, plan.midLeave, plan.finishLeave); + simulatedActions = PlacementSimulator.leave(simulatedState, toRemove); + + simulatedState = simulatedState.withStashed(simulatedActions); + + // we don't remove from bootstrapped nodes until the finish leave event executes + transformer.addOperation(this) + .markLeaving(toRemove) + .updateSimulation(simulatedState); + } + + @Override + protected void finishOperation(ModelState.Transformer transformer) + { + transformer.withLeft(targetNode()); + } + + @Override + protected void incrementCancel(ModelState.Transformer transformer) + { + transformer.incrementCancelledLeave(); + } + + protected Node targetNode() + { + return nodes[0]; + } + + @Override + public SimulatedOperation started() + { + return new Leave(nodes, sutActions, Status.STARTED, simulatedActions); + } + } + + public static class Replace extends SimulatedOperation + { + protected Replace(Node toReplace, Node replacement) + { + this(new Node[]{ toReplace, replacement }, null, Status.READY, null); + } + + public Replace(Node[] nodes, Iterator sutActions, Status status, Transformations simulatedActions) + { + super(Type.REPLACE, nodes, sutActions, status, simulatedActions); + } + + @Override + public void create(CMSSut sut, SimulatedPlacements simulatedState, ModelState.Transformer transformer) + { + assert simulatedActions == null; + + Node toReplace = nodes[0]; + Node replacement = nodes[1]; + + Optional maybePlan = prepareReplace(sut, toReplace, replacement); + if (!maybePlan.isPresent()) + { + transformer.incrementRejected(); + return; + } + + BootstrapAndReplace plan = maybePlan.get(); + sutActions = toIter(sut.service, plan.startReplace, plan.midReplace, plan.finishReplace); + simulatedActions = PlacementSimulator.replace(simulatedState, nodes[0], nodes[1]); + + transformer.addOperation(this) + .updateSimulation(simulatedState); + } + + @Override + protected void finishOperation(ModelState.Transformer transformer) + { + transformer.withReplaced(nodes[0], nodes[1]); + } + + @Override + protected void incrementCancel(ModelState.Transformer transformer) + { + transformer.incrementCancelledReplace(); + } + + protected Node targetNode() + { + return nodes[1]; + } + + @Override + public SimulatedOperation started() + { + return new Replace(nodes, sutActions, Status.STARTED, simulatedActions); + } + } + + public static class Move extends SimulatedOperation + { + protected Move(Node movingNode, Node newNode) + { + this(new Node[]{ newNode, movingNode }, null, Status.READY, null); + } + + public Move(Node[] nodes, Iterator sutActions, Status status, Transformations simulatedActions) + { + super(Type.MOVE, nodes, sutActions, status, simulatedActions); + } + + @Override + public void create(CMSSut sut, SimulatedPlacements simulatedState, ModelState.Transformer transformer) + { + assert simulatedActions == null; + Node newNode = nodes[0]; + Node movingNode = nodes[1]; + + Optional maybePlan = prepareMove(sut, movingNode, newNode.longToken()); + if (!maybePlan.isPresent()) + { + transformer.incrementRejected(); + return; + } + + org.apache.cassandra.tcm.sequences.Move plan = maybePlan.get(); + sutActions = toIter(sut.service, plan.startMove, plan.midMove, plan.finishMove); + simulatedActions = PlacementSimulator.move(simulatedState, moving(), newToken()); + + // immediately execute the first step of move transformations, the splitting of existing ranges. This + // is so that subsequent planned operations base their transformations on the split ranges. + simulatedState = simulatedActions.advance(simulatedState); + + transformer.addOperation(this) + .updateSimulation(simulatedState) + .markMoving(moving()); + } + + @Override + protected void finishOperation(ModelState.Transformer transformer) + { + transformer.withMoved(nodes[1], nodes[0]); + } + + @Override + protected void incrementCancel(ModelState.Transformer transformer) + { + transformer.incrementCancelledMove(); + } + + @Override + protected Node targetNode() + { + return nodes[0]; + } + + private long newToken() + { + return nodes[0].token(); + } + + private Node moving() + { + return nodes[1]; + } + + @Override + public SimulatedOperation started() + { + return new Move(nodes, sutActions, Status.STARTED, simulatedActions); + } + } + + private static Optional prepareJoin(CMSSut sut, Node node) + { + try + { + ClusterMetadata metadata = sut.service.commit(new PrepareJoin(node.nodeId(), + ImmutableSet.of(node.longToken()), + sut.service.placementProvider(), + true, + false)); + return Optional.of((BootstrapAndJoin) metadata.inProgressSequences.get(node.nodeId())); + } + catch (Throwable t) + { + return Optional.empty(); + } + } + + public static Optional prepareMove(CMSSut sut, Node node, LongToken newToken) + { + try + { + ClusterMetadata metadata = sut.service.commit(new PrepareMove(node.nodeId(), Collections.singleton(newToken), sut.service.placementProvider(), false)); + return Optional.of((org.apache.cassandra.tcm.sequences.Move) metadata.inProgressSequences.get(node.nodeId())); + } + catch (Throwable t) + { + return Optional.empty(); + } + } + + private static Optional prepareLeave(CMSSut sut, Node toRemove) + { + try + { + ClusterMetadata metadata = sut.service.commit(new PrepareLeave(toRemove.nodeId(), + true, + sut.service.placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + UnbootstrapAndLeave plan = (UnbootstrapAndLeave) metadata.inProgressSequences.get(toRemove.nodeId()); + return Optional.of(plan); + } + catch (Throwable e) + { + return Optional.empty(); + } + } + + private static Optional prepareReplace(CMSSut sut, Node toReplace, Node replacement) + { + try + { + ClusterMetadata result = sut.service.commit(new PrepareReplace(toReplace.nodeId(), + replacement.nodeId(), + sut.service.placementProvider(), + true, + false)); + BootstrapAndReplace plan = (BootstrapAndReplace) result.inProgressSequences.get(replacement.nodeId()); + return Optional.of(plan); + } + catch (Throwable t) + { + return Optional.empty(); + } + } + + public static Iterator toIter(ClusterMetadataService cms, Transformation... transforms) + { + Iterator iter = Iterators.forArray(transforms); + return new Iterator() + { + public boolean hasNext() + { + return iter.hasNext(); + } + + public ClusterMetadata next() + { + try + { + return cms.commit(iter.next()); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + }; + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java new file mode 100644 index 0000000000..ff3d047ef0 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SnapshotTest.java @@ -0,0 +1,244 @@ +/* + * 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.log; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.transformations.CustomTransformation; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.junit.Assert.assertEquals; + +public class SnapshotTest extends TestBaseImpl +{ + @Test + public void testSimpleSnapshot() throws Throwable + { + try (Cluster cluster = init(builder().withDC("DC1", 3) + .withDC("DC2", 3) + .withConfig(config -> config.with(NETWORK, GOSSIP) + .set("user_defined_functions_enabled", "true") + .set("materialized_views_enabled", "true")) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("CREATE OR REPLACE FUNCTION %s.fLog (input double) CALLED ON NULL INPUT RETURNS double LANGUAGE java AS 'return Double.valueOf(Math.log(input.doubleValue()));';")); + cluster.schemaChange(withKeyspace("CREATE OR REPLACE FUNCTION %s.avgState ( state tuple, val int ) CALLED ON NULL INPUT RETURNS tuple LANGUAGE java AS \n" + + " 'if (val !=null) { state.setInt(0, state.getInt(0)+1); state.setLong(1, state.getLong(1)+val.intValue()); } return state;'; ")); + cluster.schemaChange(withKeyspace("CREATE OR REPLACE FUNCTION %s.avgFinal ( state tuple ) CALLED ON NULL INPUT RETURNS double LANGUAGE java AS \n" + + " 'double r = 0; if (state.getInt(0) == 0) return null; r = state.getLong(1); r/= state.getInt(0); return Double.valueOf(r);';")); + cluster.schemaChange(withKeyspace("CREATE AGGREGATE IF NOT EXISTS %s.average ( int ) \n" + + "SFUNC avgState STYPE tuple FINALFUNC avgFinal INITCOND (0,0);")); + cluster.schemaChange(withKeyspace("CREATE MATERIALIZED VIEW %s.test_mv \n" + + "AS SELECT x, id FROM distributed_test_keyspace.tbl \n" + + "WHERE id IS NOT NULL AND x IS NOT NULL\n" + + "PRIMARY KEY (x, id)")); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadata before = ClusterMetadata.current(); + ClusterMetadata after = ClusterMetadataService.instance().sealPeriod(); + ClusterMetadata serialized = ClusterMetadataService.instance().snapshotManager().getSnapshot(after.epoch); + assertEquals(before.placements, serialized.placements); + assertEquals(before.tokenMap, serialized.tokenMap); + assertEquals(before.directory, serialized.directory); + assertEquals(before.schema, serialized.schema); + }); + + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key, x int)")); + cluster.get(1).runOnInstance(() -> { + assertEquals(2, ClusterMetadata.current().period); + }); + } + } + + @Test + public void testSnapshotReplayBootstrap() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(2) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig(config -> config.with(NETWORK, GOSSIP) + .set("user_defined_functions_enabled", "true") + .set("materialized_views_enabled", "true")) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl1 (id int primary key, x int)")); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl3 (id int primary key, x int)")); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + for (int i = 1; i <= 2; i++) + cluster.get(i).runOnInstance(() -> { assertEquals(2, ClusterMetadata.current().period); }); + + + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.startup(); + + cluster.schemaChange(withKeyspace("create table %s.tbl4 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl5 (id int primary key, x int)")); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + for (int i = 1; i <= 3; i++) + { + cluster.get(i).runOnInstance(() -> { + // no events executed after NewPeriod, period is still 2 + assertEquals(2, ClusterMetadata.current().period); + // but the next one is 3 + assertEquals(true, ClusterMetadata.current().lastInPeriod); + }); + } + + config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + newInstance = cluster.bootstrap(config); + newInstance.startup(); + + cluster.schemaChange(withKeyspace("create table %s.tbl6 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl7 (id int primary key, x int)")); + + // make sure all tables exist + for (int tbl = 1; tbl <= 7; tbl++) + for (int i = 1; i <= cluster.size(); i++) + cluster.get(i).executeInternal(withKeyspace("insert into %s.tbl"+tbl+" (id, x) values (1,1)")); + } + } + + @Test + public void testSnapshotReplayNonBootstrap() throws Throwable + { + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl1 (id int primary key, x int)")); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl3 (id int primary key, x int)")); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + for (int i = 1; i <= 3; i++) + cluster.get(i).runOnInstance(() -> { assertEquals(2, ClusterMetadata.current().period); }); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + long epochBefore = ClusterUtils.getCurrentEpoch(cluster.get(1)).getEpoch(); + + // isolate node3, perform some metadata changes and have it catch up + cluster.filters().allVerbs().to(3).drop(); + cluster.filters().allVerbs().from(3).drop(); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadataService.instance().commit(CustomTransformation.make(1)); + ClusterMetadataService.instance().commit(CustomTransformation.make(2)); + }); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1), 3); + long epochAfter = epochBefore + 2; + for (int i = 1; i <= 2; i++) + cluster.get(i).runOnInstance(() -> { assertEquals(epochAfter, ClusterMetadata.current().epoch.getEpoch()); }); + + cluster.filters().reset(); + cluster.get(3).runOnInstance(() -> { + long beforeCatchup = ClusterMetadata.current().epoch.getEpoch(); + assertEquals(epochBefore, beforeCatchup); + long afterCatchup = ClusterMetadataService.instance().processor().fetchLogAndWait().epoch.getEpoch(); + assertEquals(epochAfter, afterCatchup); + }); + + cluster.schemaChange(withKeyspace("create table %s.tbl4 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl5 (id int primary key, x int)")); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + + // make sure all tables exist + for (int tbl = 1; tbl <= 5; tbl++) + for (int i = 1; i <= cluster.size(); i++) + cluster.get(i).executeInternal(withKeyspace("insert into %s.tbl"+tbl+" (id, x) values (1,1)")); + } + } + + @Test + public void testSnapshotReplayNonBootstrapImmediatelyAfterSnapshot() throws Throwable + { + // the intention here is to observe (not actually assert anything yet) a replay where the + // requester hasn't missed any epochs *and* the last log entry sealed the period, creating + // a snapshot. We should ensure that the snapshot isn't unnecessarily sent by the CMS. + // TODO don't use snapshots during regular (non-startup) catchup at all, ensure peers + // see all individual log entries. + try (Cluster cluster = init(builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .start())) + { + cluster.schemaChange(withKeyspace("create table %s.tbl1 (id int primary key, x int)")); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + + cluster.schemaChange(withKeyspace("create table %s.tbl2 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl3 (id int primary key, x int)")); + + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + for (int i = 1; i <= 3; i++) + cluster.get(i).runOnInstance(() -> { assertEquals(2, ClusterMetadata.current().period); }); + + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + long epochBefore = ClusterUtils.getCurrentEpoch(cluster.get(1)).getEpoch(); + + cluster.get(3).runOnInstance(() -> { + long beforeCatchup = ClusterMetadata.current().epoch.getEpoch(); + assertEquals(epochBefore, beforeCatchup); + // TODO assert that this is a no-op and that the CMS doesn't actually send a snapshot + // just for the requester to drop it as the entry is already in the log + long afterCatchup = ClusterMetadataService.instance().processor().fetchLogAndWait().epoch.getEpoch(); + assertEquals(epochBefore, afterCatchup); + }); + + cluster.schemaChange(withKeyspace("create table %s.tbl4 (id int primary key, x int)")); + cluster.schemaChange(withKeyspace("create table %s.tbl5 (id int primary key, x int)")); + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1)); + + // make sure all tables exist + for (int tbl = 1; tbl <= 5; tbl++) + for (int i = 1; i <= cluster.size(); i++) + cluster.get(i).executeInternal(withKeyspace("insert into %s.tbl"+tbl+" (id, x) values (1,1)")); + } + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java new file mode 100644 index 0000000000..7cfa62ec34 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java @@ -0,0 +1,235 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.distributed.test.ExecUtil; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.log.LogState; +import org.apache.cassandra.tcm.FetchCMSLog; +import org.apache.cassandra.tcm.log.Replication; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.BiMultiValMap; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.db.SystemKeyspace.SNAPSHOT_TABLE_NAME; + +public class SystemKeyspaceStorageTest extends CoordinatorPathTestBase +{ + @Test + public void testLogStateQuery() throws Throwable + { + cmsNodeTest((cluster, simulatedCluster) -> { + // Disable snapshots on CMS "client" nodes + DatabaseDescriptor.setMetadataSnapshotFrequency(Integer.MAX_VALUE); + cluster.get(1).runOnInstance(() -> DatabaseDescriptor.setMetadataSnapshotFrequency(Integer.MAX_VALUE)); + Random rng = new Random(1L); + + // Map of epoch to period + BiMultiValMap epochToPeriod = new BiMultiValMap<>(); + // Generate some epochs + int cnt = 0; + int periodSize = rng.nextInt(10); + // set up a few epochs + for (int i = 0; i < 500; i++) + { + try + { + if (periodSize == 0) + { + cluster.get(1).runOnInstance(() -> ClusterMetadataService.instance().sealPeriod()); + ClusterMetadata metadata = ClusterMetadataService.instance().processor().fetchLogAndWait(); + epochToPeriod.put(metadata.epoch, metadata.period); + periodSize = rng.nextInt(10); + } + else + { + periodSize--; + } + ClusterMetadata metadata = ClusterMetadataService.instance().commit(CustomTransformation.make(cnt++)); + epochToPeriod.put(metadata.epoch, metadata.period); + } + catch (Throwable e) + { + throw new AssertionError(e); + } + } + + ClusterMetadataService.instance().processor().fetchLogAndWait(); + + List allEpochs = new ArrayList<>(epochToPeriod.keySet()); + List allPeriods = cluster.get(1).callOnInstance(() -> getAllSnapshots()); + List remainingPeriods = new ArrayList<>(allPeriods); + + // Delete about a half (but potentially up to 100%) of all possible snapshots + for (int i = 0; i < allPeriods.size(); i++) + { + if (rng.nextBoolean()) + { + // pick a sealed period for which we'll delete the snapshot + long toRemovePeriod = remainingPeriods.remove(rng.nextInt(remainingPeriods.size())); + // lookup the max epoch for the period, this is the key to the snapshots table + long toRemoveEpoch = maxEpochInPeriod(epochToPeriod, toRemovePeriod).getEpoch(); + cluster.get(1).runOnInstance(() -> deleteSnapshot(toRemoveEpoch)); + } + } + + repeat(10, () -> { + long lastSealed = remainingPeriods.isEmpty() ? -1 : remainingPeriods.get(rng.nextInt(remainingPeriods.size())); + if (lastSealed > 0) + { + Epoch maxInPeriod = maxEpochInPeriod(epochToPeriod, lastSealed); + cluster.get(1).runOnInstance(() -> forceLastSealed(lastSealed, maxInPeriod.getEpoch())); + } + + repeat(100, () -> { + Epoch since = allEpochs.get(rng.nextInt(allEpochs.size())); + for (boolean consistentReplay : new boolean[]{ true, false }) + { + LogState logState = simulatedCluster.node(2).requestResponse(new FetchCMSLog(since, consistentReplay)); + // Either: + // * we have no snapshots to catch up from which may + // * we've requested an epoch that's past last sealed snapshot + // * the epoch we're requesting is the max in the last sealed snapshot, in which case we + // do not send it + // so the case is equivalent to the above replication test. + if (remainingPeriods.isEmpty() + || epochToPeriod.get(since) > lastSealed + || since.is(maxEpochInPeriod(epochToPeriod, lastSealed))) + { + Assert.assertNull(logState.baseState); + assertReplication(since, allEpochs, logState.transformations); + } + // We have a snapshot to catch up from, so it has to be returned + else + { + Assert.assertEquals(lastSealed, logState.baseState.period); + Assert.assertTrue(logState.baseState.lastInPeriod); + assertReplication(logState.baseState.epoch, allEpochs, logState.transformations); + } + } + }); + }); + }); + } + + private Epoch maxEpochInPeriod(BiMultiValMap epochToPeriod, long period) + { + return epochToPeriod.inverse() + .get(period) + .stream() + .max(Comparator.naturalOrder()) + .get(); + } + + @Test + public void bounceNodeBootrappedFromSnapshot() throws Throwable + { + coordinatorPathTest(new PlacementSimulator.SimpleReplicationFactor(3), (cluster, simulatedCluster) -> { + ClusterMetadataService.instance().sealPeriod(); + ClusterMetadataService.instance().log().waitForHighestConsecutive(); + ClusterMetadataService.instance().snapshotManager().storeSnapshot(ClusterMetadata.current()); + + for (int i = 0; i < 5; i++) + ClusterMetadataService.instance().commit(CustomTransformation.make(i)); + + ClusterMetadataService.instance().log().waitForHighestConsecutive(); + + cluster.startup(); + cluster.get(1).runOnInstance(() -> { + ClusterMetadataService.instance().log().waitForHighestConsecutive(); + }); + cluster.get(1).nodetool("flush"); + FBUtilities.waitOnFuture(cluster.get(1).shutdown()); + cluster.get(1).startup(); + // TODO: currently, we do not have means of stopping traffic between the CMS and node1 + // When we do, we need to prevent replay handler from returning results, and assert node1 still catches up + }, false); + } + + public static void assertReplication(Epoch since, List allEpochs, Replication replication) + { + // +1 because we assume we already know the `since` epoch + int offset = Collections.binarySearch(allEpochs, since) + 1; + + if (since.equals(allEpochs.get(allEpochs.size() - 1))) + Assert.assertEquals(0, replication.entries().size()); + else + Assert.assertEquals(since.getEpoch() + 1, replication.entries().get(0).epoch.getEpoch()); + + Assert.assertEquals(allEpochs.get(allEpochs.size() - 1).getEpoch() - since.getEpoch(), + replication.entries().size()); + for (int i = 0; i < replication.entries().size(); i++) + { + Assert.assertEquals(String.format("Got mismatch while replication starting with %s", since), + allEpochs.get(offset + i), replication.entries().get(i).epoch); + } + } + + public static void repeat(int num, ExecUtil.ThrowingSerializableRunnable r) + { + for (int i = 0; i < num; i++) + { + try + { + r.run(); + } + catch (Throwable throwable) + { + throw new AssertionError(throwable); + } + } + } + + public static List getAllSnapshots() + { + List allPeriods = new ArrayList<>(); + String query = String.format("SELECT period FROM %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); + for (UntypedResultSet.Row row : QueryProcessor.executeInternal(query)) + allPeriods.add(row.getLong("period")); + + return allPeriods; + } + + public static void deleteSnapshot(long epoch) + { + String query = String.format("DELETE FROM %s.%s WHERE epoch = ?", SchemaConstants.SYSTEM_KEYSPACE_NAME, SNAPSHOT_TABLE_NAME); + QueryProcessor.executeInternal(query, epoch); + } + + public static void forceLastSealed(long period, long epoch) + { + Sealed.unsafeClearLookup(); + Sealed.recordSealedPeriod(period, Epoch.create(epoch)); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java b/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java new file mode 100644 index 0000000000..f5fabfb4ac --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java @@ -0,0 +1,137 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Processor; +import org.apache.cassandra.tcm.Retry; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.log.Entry; +import org.apache.cassandra.utils.concurrent.WaitQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestProcessor implements Processor +{ + private static final Logger logger = LoggerFactory.getLogger(TestProcessor.class); + private final AtomicBoolean isPaused = new AtomicBoolean(); + private final WaitQueue waiters; + private final List> waitPredicates; + private final List> commitPredicates; + private final Processor delegate; + + public TestProcessor(Processor delegate) + { + this.waiters = WaitQueue.newWaitQueue(); + this.waitPredicates = new ArrayList<>(); + this.commitPredicates = new ArrayList<>(); + this.delegate = delegate; + } + + @Override + public Commit.Result commit(Entry.Id entryId, Transformation transform, Epoch lastKnown, Retry.Deadline retryPolicy) + { + maybePause(transform); + waitIfPaused(); + Commit.Result commited = delegate.commit(entryId, transform, lastKnown, retryPolicy); + testCommitPredicates(transform, commited); + return commited; + } + + @Override + public ClusterMetadata fetchLogAndWait(Epoch waitFor, Retry.Deadline retryPolicy) + { + return delegate.fetchLogAndWait(waitFor, retryPolicy); + } + + protected void waitIfPaused() + { + if (isPaused()) + { + logger.debug("Test processor is paused, waiting..."); + WaitQueue.Signal signal = waiters.register(); + if (!isPaused()) + signal.cancel(); + else + signal.awaitUninterruptibly(); + logger.debug("Resumed test processor..."); + } + } + + public boolean isPaused() + { + return isPaused.get(); + } + + public void pauseIf(Predicate predicate, Runnable onMatch) + { + waitPredicates.add((e) -> { + if (predicate.test(e)) + { + onMatch.run(); + return true; + } + return false; + }); + } + + public void registerCommitPredicate(BiFunction fn) + { + commitPredicates.add(fn); + } + + private void maybePause(Transformation transform) + { + Iterator> iter = waitPredicates.iterator(); + + while (iter.hasNext()) + { + if (iter.next().test(transform)) + { + pause(); + iter.remove(); + } + } + } + + private void testCommitPredicates(Transformation transform, Commit.Result result) + { + commitPredicates.removeIf(predicate -> predicate.apply(transform, result)); + } + + public void pause() + { + isPaused.set(true); + } + + public void unpause() + { + isPaused.set(false); + waiters.signalAll(); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/TriggeredReconfigureCMSTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/TriggeredReconfigureCMSTest.java new file mode 100644 index 0000000000..1f46e04e1d --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/log/TriggeredReconfigureCMSTest.java @@ -0,0 +1,151 @@ +/* + * 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.log; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.gms.FailureDetector; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; + +import static org.apache.cassandra.distributed.Constants.KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN; +import static org.apache.cassandra.distributed.Constants.KEY_DTEST_FULL_STARTUP; +import static org.apache.cassandra.distributed.api.TokenSupplier.evenlyDistributedTokens; +import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance; +import static org.apache.cassandra.distributed.shared.ClusterUtils.getCMSMembers; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +// Tests for operations such as remove, decomission, and replace triggering CMS reconfiguration +public class TriggeredReconfigureCMSTest extends FuzzTestBase +{ + @Test + public void testRemoveCMSMember() throws IOException, ExecutionException, InterruptedException + { + try (Cluster cluster = Cluster.build(5) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + cluster.get(1).nodetoolResult("reconfigurecms", "--sync", "3").asserts().success(); + Set cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + + String instanceToRemove = cms.stream().filter(addr -> !addr.contains("/127.0.0.1")).findFirst().get(); + IInvokableInstance nodeToRemove = cluster.stream().filter(i -> i.config().broadcastAddress().toString().contains(instanceToRemove)).findFirst().get(); + String nodeId = nodeToRemove.callOnInstance(() -> ClusterMetadata.current().myNodeId().toUUID().toString()); + nodeToRemove.shutdown().get(); + + cluster.get(1).runOnInstance(() -> { + try + { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (FailureDetector.instance.isAlive(InetAddressAndPort.getByName(instanceToRemove.replace("/", ""))) && + System.nanoTime() < deadline) + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + }); + cluster.get(1).nodetoolResult("removenode", nodeId, "--force").asserts().success(); + + cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + assertTrue(cms.contains("/127.0.0.1")); + assertFalse(cms.contains(instanceToRemove)); + } + } + + @Test + public void testDecommissionCMSMember() throws IOException + { + try (Cluster cluster = Cluster.build(5) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + cluster.get(1).nodetoolResult("reconfigurecms", "--sync", "3").asserts().success(); + Set cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + cluster.get(1).nodetoolResult("decommission").asserts().success(); + cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + } + } + + @Test + public void testReplaceCMSMember() throws IOException + { + try (Cluster cluster = Cluster.build(5) + .withTokenSupplier(evenlyDistributedTokens(6, 1)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(5, "dc0", "rack0")) + .withConfig(conf -> conf.set("hinted_handoff_enabled", "false") + .with(Feature.NETWORK, Feature.GOSSIP)) + .start()) + { + cluster.get(1).nodetoolResult("reconfigurecms", "--sync", "3").asserts().success(); + Set cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + assertTrue(cms.contains("/127.0.0.1")); + + IInvokableInstance toReplace = cluster.get(2); + Collection replacedTokens = ClusterUtils.getLocalTokens(toReplace); + toReplace.shutdown(false); + IInvokableInstance replacement = addInstance(cluster, + toReplace.config(), + c -> c.set("auto_bootstrap", true) + .set(KEY_DTEST_FULL_STARTUP, false) + .set(KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false)); + try (WithProperties replacementProps = new WithProperties()) + { + replacementProps.set(CassandraRelevantProperties.REPLACE_ADDRESS_FIRST_BOOT, + toReplace.config().broadcastAddress().getAddress().getHostAddress()); + replacement.startup(); + } + + Collection replacementTokens = ClusterUtils.getLocalTokens(replacement); + assertEquals(replacedTokens, replacementTokens); + + cms = getCMSMembers(cluster.get(1)); + assertEquals(3, cms.size()); + assertTrue(cms.contains("/127.0.0.1")); + assertFalse(cms.contains("/127.0.0.2")); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/AssignSameTokenToMultipleNodesTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/AssignSameTokenToMultipleNodesTest.java new file mode 100644 index 0000000000..a891e1876b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/AssignSameTokenToMultipleNodesTest.java @@ -0,0 +1,52 @@ +/* + * 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.ring; + +import java.util.Collections; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.test.TestBaseImpl; + +public class AssignSameTokenToMultipleNodesTest extends TestBaseImpl +{ + @Test + public void testAssignSameTokenToMultipleNodes() throws Throwable + { + try (Cluster cluster = builder().withNodes(2) + .withTokenSupplier((TokenSupplier) i -> Collections.singleton("0")) + .createWithoutStarting()) + { + cluster.get(1).startup(); + try + { + cluster.get(2).startup(); + Assert.fail("Startup should have failed"); + } + catch (Throwable t) + { + Assert.assertTrue(t.getClass().getName().contains("ConfigurationException")); + Assert.assertTrue(t.getMessage().contains("Bootstrapping to existing token 0 is not allowed")); + } + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapResetProgressTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapResetProgressTest.java new file mode 100644 index 0000000000..9ad41b0dba --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapResetProgressTest.java @@ -0,0 +1,156 @@ +/* + * 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.ring; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.AssumptionViolatedException; +import org.junit.Test; + +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; +import static org.apache.cassandra.config.CassandraRelevantProperties.RESET_BOOTSTRAP_PROGRESS; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +public class BootstrapResetProgressTest extends TestBaseImpl +{ + /** + * Confirm that, in the absence of the reset_bootstrap_progress param being set and in the face of a found prior + * partial bootstrap, we error out and don't complete our bootstrap. + * + * Test w/out vnodes only; logic is identical for both run env but the token alloc in this test doesn't work for + * vnode env and it's not worth the lift to update it to work in both env. + * + * @throws Throwable + */ + @Test + public void bootstrapUnspecifiedFailsOnResumeTest() throws Throwable + { + RESET_BOOTSTRAP_PROGRESS.reset(); + + // Need our partitioner active for rangeToBytes conversion below + Config c = DatabaseDescriptor.loadConfig(); + DatabaseDescriptor.daemonInitialization(() -> c); + + int originalNodeCount = 2; + int expandedNodeCount = originalNodeCount + 1; + + boolean sawException = false; + try (Cluster cluster = builder().withNodes(originalNodeCount) + .withoutVNodes() + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(expandedNodeCount)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0")) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .start()) + { + BootstrapTest.populate(cluster, 0, 100); + + IInstanceConfig config = cluster.newInstanceConfig().set("auto_bootstrap", "true"); + IInvokableInstance newInstance = cluster.bootstrap(config); + newInstance.runOnInstance(() -> { + JOIN_RING.setBoolean(false); + }); + newInstance.startup(); + + List tokens = cluster.tokens(); + assert tokens.size() >= 3; + + /* + Our local tokens: + Tokens in cluster tokens: [-3074457345618258603, 3074457345618258601, 9223372036854775805] + + From the bootstrap process: + fetchReplicas in our test keyspace: + [FetchReplica + {local=Full(/127.0.0.3:7012,(-3074457345618258603,3074457345618258601]), + remote=Full(/127.0.0.1:7012,(-3074457345618258603,3074457345618258601])}, + FetchReplica + {local=Full(/127.0.0.3:7012,(9223372036854775805,-3074457345618258603]), + remote=Full(/127.0.0.1:7012,(9223372036854775805,-3074457345618258603])}, + FetchReplica + {local=Full(/127.0.0.3:7012,(3074457345618258601,9223372036854775805]), + remote=Full(/127.0.0.1:7012,(3074457345618258601,9223372036854775805])}] + */ + + // Insert some bogus ranges in the keyspace to be bootstrapped to trigger the check on available ranges on bootstrap. + // Note: these have to precisely overlap with the token ranges hit during streaming or they won't trigger the + // availability logic on bootstrap to then except out; we can't just have _any_ range for a keyspace, but rather, + // must have a range that overlaps with what we're trying to stream. + Set> fullSet = new HashSet<>(); + fullSet.add(new Range<>(tokens.get(0), tokens.get(1))); + fullSet.add(new Range<>(tokens.get(1), tokens.get(2))); + fullSet.add(new Range<>(tokens.get(2), tokens.get(0))); + + // Should be fine to trigger on full ranges only but add a partial for good measure. + Set> partialSet = new HashSet<>(); + partialSet.add(new Range<>(tokens.get(2), tokens.get(1))); + + String cql = String.format("INSERT INTO %s.%s (keyspace_name, full_ranges, transient_ranges) VALUES (?, ?, ?)", + SchemaConstants.SYSTEM_KEYSPACE_NAME, + SystemKeyspace.AVAILABLE_RANGES_V2); + + newInstance.executeInternal(cql, + KEYSPACE, + fullSet.stream().map(SystemKeyspace::rangeToBytes).collect(Collectors.toSet()), + partialSet.stream().map(SystemKeyspace::rangeToBytes).collect(Collectors.toSet())); + + newInstance.runOnInstance(() -> { + try + { + StorageService.instance.joinRing(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + }); + } + catch (AssumptionViolatedException ave) + { + // We get an AssumptionViolatedException if we're in a test job configured w/vnodes + throw ave; + } + catch (RuntimeException rte) + { + if (rte.getMessage().contains("Discovered existing bootstrap data")) + sawException = true; + } + Assert.assertTrue("Expected to see a RuntimeException w/'Discovered existing bootstrap data' in the error message; did not.", + sawException); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapTest.java index 5a042251bb..ecdb152f89 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/BootstrapTest.java @@ -18,51 +18,31 @@ package org.apache.cassandra.distributed.test.ring; -import java.lang.management.ManagementFactory; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.Callable; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.junit.After; -import org.junit.Assert; -import org.junit.AssumptionViolatedException; -import org.junit.Before; import org.junit.Test; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.SuperCall; -import org.apache.cassandra.config.Config; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.shared.NetworkTopology; -import org.apache.cassandra.distributed.shared.WithProperties; -import org.apache.cassandra.distributed.test.DecommissionTest; import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.StorageService; -import static java.util.Arrays.asList; import static net.bytebuddy.matcher.ElementMatchers.named; -import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; -import static org.apache.cassandra.config.CassandraRelevantProperties.MIGRATION_DELAY; import static org.apache.cassandra.config.CassandraRelevantProperties.RESET_BOOTSTRAP_PROGRESS; -import static org.apache.cassandra.distributed.action.GossipHelper.bootstrap; -import static org.apache.cassandra.distributed.action.GossipHelper.pullSchemaFrom; -import static org.apache.cassandra.distributed.action.GossipHelper.statusToBootstrap; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_WRITE_SURVEY; import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; @@ -70,30 +50,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; + public class BootstrapTest extends TestBaseImpl { - private long savedMigrationDelay; - - static WithProperties properties; - - @Before - public void beforeTest() - { - // MigrationCoordinator schedules schema pull requests immediatelly when the node is just starting up, otherwise - // the first pull request is sent in 60 seconds. Whether we are starting up or not is detected by examining - // the node up-time and if it is lower than MIGRATION_DELAY, we consider the server is starting up. - // When we are running multiple test cases in the class, where each starts a node but in the same JVM, the - // up-time will be more or less relevant only for the first test. In order to enforce the startup-like behaviour - // for each test case, the MIGRATION_DELAY time is adjusted accordingly - properties = new WithProperties().set(MIGRATION_DELAY, ManagementFactory.getRuntimeMXBean().getUptime() + savedMigrationDelay); - } - - @After - public void afterTest() - { - properties.close(); - } - @Test public void bootstrapWithResumeTest() throws Throwable { @@ -119,105 +78,10 @@ public class BootstrapTest extends TestBaseImpl bootstrapTest(); } - /** - * Confirm that, in the absence of the reset_bootstrap_progress param being set and in the face of a found prior - * partial bootstrap, we error out and don't complete our bootstrap. - * - * Test w/out vnodes only; logic is identical for both run env but the token alloc in this test doesn't work for - * vnode env and it's not worth the lift to update it to work in both env. - * - * @throws Throwable - */ - @Test - public void bootstrapUnspecifiedFailsOnResumeTest() throws Throwable - { - RESET_BOOTSTRAP_PROGRESS.clearValue(); // checkstyle: suppress nearby 'clearValueSystemPropertyUsage' - - // Need our partitioner active for rangeToBytes conversion below - Config c = DatabaseDescriptor.loadConfig(); - DatabaseDescriptor.daemonInitialization(() -> c); - - int originalNodeCount = 2; - int expandedNodeCount = originalNodeCount + 1; - - boolean sawException = false; - try (Cluster cluster = builder().withNodes(originalNodeCount) - .withoutVNodes() - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(expandedNodeCount)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP)) - .start()) - { - populate(cluster, 0, 100); - - IInstanceConfig config = cluster.newInstanceConfig(); - IInvokableInstance newInstance = cluster.bootstrap(config); - withProperty(JOIN_RING, false, () -> newInstance.startup(cluster)); - cluster.forEach(statusToBootstrap(newInstance)); - - List tokens = cluster.tokens(); - assert tokens.size() >= 3; - - /* - Our local tokens: - Tokens in cluster tokens: [-3074457345618258603, 3074457345618258601, 9223372036854775805] - - From the bootstrap process: - fetchReplicas in our test keyspace: - [FetchReplica - {local=Full(/127.0.0.3:7012,(-3074457345618258603,3074457345618258601]), - remote=Full(/127.0.0.1:7012,(-3074457345618258603,3074457345618258601])}, - FetchReplica - {local=Full(/127.0.0.3:7012,(9223372036854775805,-3074457345618258603]), - remote=Full(/127.0.0.1:7012,(9223372036854775805,-3074457345618258603])}, - FetchReplica - {local=Full(/127.0.0.3:7012,(3074457345618258601,9223372036854775805]), - remote=Full(/127.0.0.1:7012,(3074457345618258601,9223372036854775805])}] - */ - - // Insert some bogus ranges in the keyspace to be bootstrapped to trigger the check on available ranges on bootstrap. - // Note: these have to precisely overlap with the token ranges hit during streaming or they won't trigger the - // availability logic on bootstrap to then except out; we can't just have _any_ range for a keyspace, but rather, - // must have a range that overlaps with what we're trying to stream. - Set> fullSet = new HashSet<>(); - fullSet.add(new Range<>(tokens.get(0), tokens.get(1))); - fullSet.add(new Range<>(tokens.get(1), tokens.get(2))); - fullSet.add(new Range<>(tokens.get(2), tokens.get(0))); - - // Should be fine to trigger on full ranges only but add a partial for good measure. - Set> partialSet = new HashSet<>(); - partialSet.add(new Range<>(tokens.get(2), tokens.get(1))); - - String cql = String.format("INSERT INTO %s.%s (keyspace_name, full_ranges, transient_ranges) VALUES (?, ?, ?)", - SchemaConstants.SYSTEM_KEYSPACE_NAME, - SystemKeyspace.AVAILABLE_RANGES_V2); - - newInstance.executeInternal(cql, - KEYSPACE, - fullSet.stream().map(SystemKeyspace::rangeToBytes).collect(Collectors.toSet()), - partialSet.stream().map(SystemKeyspace::rangeToBytes).collect(Collectors.toSet())); - - // We expect bootstrap to throw an exception on node3 w/the seen ranges we've inserted - cluster.run(asList(pullSchemaFrom(cluster.get(1)), - bootstrap()), - newInstance.config().num()); - } - catch (AssumptionViolatedException ave) - { - // We get an AssumptionViolatedException if we're in a test job configured w/vnodes - throw ave; - } - catch (RuntimeException rte) - { - if (rte.getMessage().contains("Discovered existing bootstrap data")) - sawException = true; - } - Assert.assertTrue("Expected to see a RuntimeException w/'Discovered existing bootstrap data' in the error message; did not.", - sawException); - } - private void bootstrapTest() throws Throwable { + // This test simply asserts that the value of the cassandra.reset_bootstrap_progress flag + // has no impact on a normal, uninterrupted bootstrap int originalNodeCount = 2; int expandedNodeCount = originalNodeCount + 1; @@ -229,19 +93,14 @@ public class BootstrapTest extends TestBaseImpl { populate(cluster, 0, 100); - IInstanceConfig config = cluster.newInstanceConfig(); + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); IInvokableInstance newInstance = cluster.bootstrap(config); - withProperty(JOIN_RING, false, - () -> newInstance.startup(cluster)); - - cluster.forEach(statusToBootstrap(newInstance)); - - cluster.run(asList(pullSchemaFrom(cluster.get(1)), - bootstrap()), - newInstance.config().num()); + newInstance.startup(cluster); for (Map.Entry e : count(cluster).entrySet()) - Assert.assertEquals("Node " + e.getKey() + " has incorrect row state", + assertEquals("Node " + e.getKey() + " has incorrect row state", 100L, e.getValue().longValue()); } @@ -261,14 +120,10 @@ public class BootstrapTest extends TestBaseImpl { IInstanceConfig config = cluster.newInstanceConfig(); IInvokableInstance newInstance = cluster.bootstrap(config); - withProperty(JOIN_RING, false, + withProperty(TEST_WRITE_SURVEY, true, () -> newInstance.startup(cluster)); - - cluster.forEach(statusToBootstrap(newInstance)); - populate(cluster, 0, 100); - - Assert.assertEquals(100, newInstance.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); + assertEquals(100, newInstance.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); } } @@ -278,7 +133,6 @@ public class BootstrapTest extends TestBaseImpl int originalNodeCount = 2; int expandedNodeCount = originalNodeCount + 1; - RESET_BOOTSTRAP_PROGRESS.setBoolean(false); try (Cluster cluster = builder().withNodes(originalNodeCount) .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(expandedNodeCount)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0")) @@ -313,11 +167,22 @@ public class BootstrapTest extends TestBaseImpl { cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + rf + "};"); cluster.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + populateExistingTable(cluster, from, to, coord, cl); for (int i = from; i < to; i++) { - cluster.coordinator(coord).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, ?, ?)", - cl, - i, i, i); + cluster.coordinator(coord).executeWithRetries("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, ?, ?)", + cl, + i, i, i); + } + } + + public static void populateExistingTable(ICluster cluster, int from, int to, int coord, ConsistencyLevel cl) + { + for (int i = from; i < to; i++) + { + cluster.coordinator(coord).executeWithRetries("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, ?, ?)", + cl, + i, i, i); } } @@ -338,8 +203,8 @@ public class BootstrapTest extends TestBaseImpl return; } new ByteBuddy().rebase(StorageService.class) - .method(named("bootstrapFinished")) - .intercept(MethodDelegation.to(DecommissionTest.BB.class)) + .method(named("markViewsAsBuilt")) + .intercept(MethodDelegation.to(BB.class)) .make() .load(classLoader, ClassLoadingStrategy.Default.INJECTION); } @@ -347,7 +212,7 @@ public class BootstrapTest extends TestBaseImpl private static int invocations = 0; @SuppressWarnings("unused") - public static void bootstrapFinished(@SuperCall Callable zuper) + public static void markViewsAsBuilt(@SuperCall Callable zuper) { ++invocations; diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/CMSMembershipTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/CMSMembershipTest.java new file mode 100644 index 0000000000..6f07e7adfd --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/CMSMembershipTest.java @@ -0,0 +1,185 @@ +/* + * 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.ring; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.cms.RemoveFromCMS; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.getCMSMembers; + +public class CMSMembershipTest extends FuzzTestBase +{ + @Test + public void joinCmsTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3).start()) + { + for (int idx : new int[]{ 2, 3 }) + { + cluster.get(idx).runOnInstance(() -> { + AddToCMS.initiate(); + ClusterMetadataService.instance().commit(CustomTransformation.make(idx)); + }); + } + } + } + + @Test + public void expandCmsTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3).start()) + { + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + for (NodeId nodeId : metadata.directory.peerIds()) + { + if (nodeId.equals(metadata.myNodeId())) + continue; + AddToCMS.initiate(nodeId, metadata.directory.getNodeAddresses(nodeId).broadcastAddress); + } + }); + + for (int idx : new int[]{ 1, 2, 3 }) + { + cluster.get(idx).runOnInstance(() -> { + ClusterMetadataService.instance().commit(CustomTransformation.make(idx)); + }); + } + + for (int idx : new int[]{ 1, 2, 3 }) + { + cluster.get(idx).runOnInstance(() -> { + ClusterMetadataService.instance().processor().fetchLogAndWait(); + ClusterMetadata metadata = ClusterMetadata.current(); + Assert.assertTrue(metadata.fullCMSMembers().contains(FBUtilities.getBroadcastAddressAndPort())); + }); + } + } + } + + @Test + public void shrinkCmsTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3).start()) + { + cluster.get(1).runOnInstance(() -> { + try + { + // When there's only one CMS node left, we should reject even `forced` transformatiosn + ClusterMetadataService.instance().commit(new RemoveFromCMS(FBUtilities.getBroadcastAddressAndPort(), true)); + } + catch (IllegalStateException e) + { + if (!e.getMessage().contains("would leave no members in CMS")) + throw new AssertionError(e.getMessage()); + } + }); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + for (NodeId nodeId : metadata.directory.peerIds()) + { + if (nodeId.equals(metadata.myNodeId())) + continue; + AddToCMS.initiate(nodeId, metadata.directory.getNodeAddresses(nodeId).broadcastAddress); + } + }); + + Set initialCMS = getCMSMembers(cluster.get(1)); + Assert.assertEquals(3, initialCMS.size()); + for (int i=2; i<=3; i++) + { + cluster.get(i).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + Assert.assertEquals(initialCMS, getCMSMembers(cluster.get(i))); + } + + // Without the force option set, removing a node from the CMS should + // be rejected if it would cause the size to go below 3 nodes + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + for (InetAddressAndPort addr : metadata.directory.allAddresses()) + { + if (addr.toString().contains("127.0.0.3")) + { + try + { + ClusterMetadataService.instance().commit(new RemoveFromCMS(addr, false)); + } + catch (IllegalStateException e) + { + if (!e.getMessage().contains("resubmit with force=true")) + throw new AssertionError(e.getMessage()); + } + return; + } + } + }); + + // expect no changes to the CMS membership yet + for (int i=1; i<=3; i++) + { + cluster.get(i).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + Assert.assertEquals(initialCMS, getCMSMembers(cluster.get(i))); + } + + // Run again, this time with the force option + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + for (InetAddressAndPort addr : metadata.directory.allAddresses()) + { + if (addr.toString().contains("127.0.0.3")) + { + ClusterMetadataService.instance().commit(new RemoveFromCMS(addr, true)); + return; + } + } + }); + + // node3 should have been removed from the CMS + Set updatedCMS = initialCMS.stream().filter(s -> !s.contains("127.0.0.3")).collect(Collectors.toSet()); + for (int i=1; i<=3; i++) + { + cluster.get(i).runOnInstance(() -> ClusterMetadataService.instance().processor().fetchLogAndWait()); + Assert.assertEquals(updatedCMS, getCMSMembers(cluster.get(i))); + } + + // Commit some transformations as a smoke test + for (int idx : new int[]{ 1, 2, 3 }) + { + cluster.get(idx).runOnInstance(() -> { + ClusterMetadataService.instance().commit(CustomTransformation.make(idx)); + }); + } + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/CleanupFailureTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/CleanupDuringRangeMovementTest.java similarity index 53% rename from test/distributed/org/apache/cassandra/distributed/test/ring/CleanupFailureTest.java rename to test/distributed/org/apache/cassandra/distributed/test/ring/CleanupDuringRangeMovementTest.java index 8ec3630983..bc46a1fe7b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/CleanupFailureTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/CleanupDuringRangeMovementTest.java @@ -18,64 +18,86 @@ package org.apache.cassandra.distributed.test.ring; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + import org.junit.Test; import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.NodeToolResult; import org.apache.cassandra.distributed.shared.NetworkTopology; import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; -import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; +import static org.apache.cassandra.distributed.shared.ClusterUtils.decommission; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; +import static org.apache.cassandra.distributed.test.ring.BootstrapTest.populateExistingTable; import static org.junit.Assert.assertEquals; -import static org.apache.cassandra.distributed.action.GossipHelper.statusToBootstrap; -import static org.apache.cassandra.distributed.action.GossipHelper.statusToDecommission; -import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; -import static org.apache.cassandra.distributed.test.ring.BootstrapTest.populate; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.api.TokenSupplier.evenlyDistributedTokens; +import static org.junit.Assert.assertTrue; -public class CleanupFailureTest extends TestBaseImpl +public class CleanupDuringRangeMovementTest extends TestBaseImpl { @Test public void cleanupDuringDecommissionTest() throws Throwable { + ExecutorService executor = Executors.newSingleThreadExecutor(); try (Cluster cluster = builder().withNodes(2) .withTokenSupplier(evenlyDistributedTokens(2)) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(2, "dc0", "rack0")) .withConfig(config -> config.with(NETWORK, GOSSIP)) .start()) { - IInvokableInstance nodeToDecommission = cluster.get(1); - IInvokableInstance nodeToRemainInCluster = cluster.get(2); + IInvokableInstance cmsInstance = cluster.get(1); + IInvokableInstance nodeToDecommission = cluster.get(2); + IInvokableInstance nodeToRemainInCluster = cluster.get(1); // CMS instance remains in the cluster + // Create table before starting decommission as at the moment schema changes are not permitted + // while range movements are in-flight. Additionally, pausing the CMS instance to block the + // leave sequence from completing would also block the commit of the schema transformation + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); + cluster.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + + // Prime the CMS node to pause before the finish leave event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareLeave.FinishLeave); // Start decomission on nodeToDecommission - cluster.forEach(statusToDecommission(nodeToDecommission)); + Future decomFuture = executor.submit(() -> decommission(nodeToDecommission)); + pending.call(); // Add data to cluster while node is decomissioning int NUM_ROWS = 100; - populate(cluster, 0, NUM_ROWS, 1, 1, ConsistencyLevel.ONE); + populateExistingTable(cluster, 0, NUM_ROWS, 2, ConsistencyLevel.ONE); cluster.forEach(c -> c.flush(KEYSPACE)); // Check data before cleanup on nodeToRemainInCluster - assertEquals(100, nodeToRemainInCluster.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); + assertEquals(NUM_ROWS, nodeToRemainInCluster.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); // Run cleanup on nodeToRemainInCluster NodeToolResult result = nodeToRemainInCluster.nodetoolResult("cleanup"); - result.asserts().failure(); - result.asserts().stderrContains("Node is involved in cluster membership changes. Not safe to run cleanup."); + result.asserts().success(); // Check data after cleanup on nodeToRemainInCluster - assertEquals(100, nodeToRemainInCluster.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); + assertEquals(NUM_ROWS, nodeToRemainInCluster.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); + + unpauseCommits(cmsInstance); + assertTrue(decomFuture.get()); } } @Test public void cleanupDuringBootstrapTest() throws Throwable { + ExecutorService executor = Executors.newSingleThreadExecutor(); int originalNodeCount = 1; int expandedNodeCount = originalNodeCount + 1; @@ -85,17 +107,26 @@ public class CleanupFailureTest extends TestBaseImpl .withConfig(config -> config.with(NETWORK, GOSSIP)) .start()) { - IInstanceConfig config = cluster.newInstanceConfig(); - IInvokableInstance bootstrappingNode = cluster.bootstrap(config); - withProperty(JOIN_RING, false, - () -> bootstrappingNode.startup(cluster)); + // Create table before starting bootstrap as at the moment schema changes are not permitted + // while range movements are in-flight. Additionally, pausing the CMS instance to block the + // leave sequence from completing would also block the commit of the schema transformation + cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};"); + cluster.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); - // Start decomission on bootstrappingNode - cluster.forEach(statusToBootstrap(bootstrappingNode)); + IInvokableInstance cmsInstance = cluster.get(1); + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance bootstrappingNode = cluster.bootstrap(config); + + // Prime the CMS node to pause before the finish join event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.FinishJoin); + Future bootstrapFuture = executor.submit(() -> bootstrappingNode.startup()); + pending.call(); // Add data to cluster while node is bootstrapping int NUM_ROWS = 100; - populate(cluster, 0, NUM_ROWS, 1, 2, ConsistencyLevel.ONE); + populateExistingTable(cluster, 0, NUM_ROWS, 1, ConsistencyLevel.ONE); cluster.forEach(c -> c.flush(KEYSPACE)); // Check data before cleanup on bootstrappingNode @@ -103,10 +134,12 @@ public class CleanupFailureTest extends TestBaseImpl // Run cleanup on bootstrappingNode NodeToolResult result = bootstrappingNode.nodetoolResult("cleanup"); - result.asserts().stderrContains("Node is involved in cluster membership changes. Not safe to run cleanup."); + result.asserts().success(); // Check data after cleanup on bootstrappingNode assertEquals(NUM_ROWS, bootstrappingNode.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl").length); + unpauseCommits(cmsInstance); + bootstrapFuture.get(); } } } \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/CommunicationDuringDecommissionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/CommunicationDuringDecommissionTest.java index 631ea9113a..f8d4c9ede7 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/CommunicationDuringDecommissionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/CommunicationDuringDecommissionTest.java @@ -27,9 +27,9 @@ import org.junit.Assert; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.test.TestBaseImpl; -import static org.apache.cassandra.distributed.action.GossipHelper.decommission; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; import static org.apache.cassandra.distributed.api.Feature.NETWORK; @@ -46,29 +46,31 @@ public class CommunicationDuringDecommissionTest extends TestBaseImpl { BootstrapTest.populate(cluster, 0, 100); - cluster.run(decommission(), 1); - - cluster.filters().allVerbs().from(1).messagesMatching((i, i1, iMessage) -> { + int decomissioningNode = 4; + cluster.get(decomissioningNode).nodetoolResult("decommission").asserts().success(); + cluster.filters().allVerbs().from(decomissioningNode).messagesMatching((i, i1, iMessage) -> { throw new AssertionError("Decomissioned node should not send any messages"); }).drop(); - + ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1), decomissioningNode); Map connectionAttempts = new HashMap<>(); long deadline = currentTimeMillis() + TimeUnit.SECONDS.toMillis(10); // Wait 10 seconds and check if there are any new connection attempts to the decomissioned node while (currentTimeMillis() <= deadline) { - for (int i = 2; i <= cluster.size(); i++) + for (int i = 1; i <= 3; i++) { - Object[][] res = cluster.get(i).executeInternal("SELECT active_connections, connection_attempts FROM system_views.internode_outbound WHERE address = '127.0.0.1' AND port = 7012"); + Object[][] res = cluster.get(i).executeInternal("SELECT active_connections, connection_attempts FROM system_views.internode_outbound WHERE address = '127.0.0.4' AND port = 7012"); Assert.assertEquals(1, res.length); Assert.assertEquals(0L, ((Long) res[0][0]).longValue()); long attempts = ((Long) res[0][1]).longValue(); if (connectionAttempts.get(i) == null) connectionAttempts.put(i, attempts); else - Assert.assertEquals(connectionAttempts.get(i), (Long) attempts); + Assert.assertEquals(String.format("Number of connection attempts from %d to %d should have been stable but changed from %d to %d", + i, decomissioningNode, connectionAttempts.get(i), attempts), + connectionAttempts.get(i), (Long) attempts); } LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java new file mode 100644 index 0000000000..5600e17881 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java @@ -0,0 +1,251 @@ +/* + * 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.ring; + +import java.util.concurrent.Callable; + +import org.junit.Assert; +import org.junit.Test; + +import harry.core.Configuration; +import harry.core.Run; +import harry.operations.Query; +import harry.visitors.LoggingVisitor; +import harry.visitors.MutatingRowVisitor; +import harry.visitors.MutatingVisitor; +import harry.visitors.Visitor; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.metrics.TCMMetrics; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.PrepareJoin; + +import static org.apache.cassandra.distributed.shared.ClusterUtils.getSequenceAfterCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; +import static org.apache.cassandra.distributed.shared.ClusterUtils.waitForCMSToQuiesce; + +public class ConsistentBootstrapTest extends FuzzTestBase +{ + private static int WRITES = 2000; + + private static final Configuration.ConfigurationBuilder configBuilder; + + static + { + try + { + configBuilder = HarryHelper.defaultConfiguration() + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1)) + .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Test + public void bootstrapFuzzTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP).set("metadata_snapshot_frequency", 5)) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + waitForCMSToQuiesce(cluster, cmsInstance); + configBuilder.setSUT(() -> new InJvmSut(cluster)); + Run run = configBuilder.build().createRun(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + waitForCMSToQuiesce(cluster, cluster.get(1)); + Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new); + QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run); + System.out.println("Starting write phase..."); + for (int i = 0; i < WRITES; i++) + visitor.visit(); + System.out.println("Starting validate phase..."); + for (int lts = 0; lts < run.clock.peek(); lts++) + model.validate(Query.selectPartition(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), false)); + + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true); + IInvokableInstance newInstance = cluster.bootstrap(config); + + // Prime the CMS node to pause before the finish join event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.FinishJoin); + new Thread(() -> newInstance.startup()).start(); + pending.call(); + + for (int i = 0; i < WRITES; i++) + visitor.visit(); + + try + { + for (int lts = 0; lts < run.clock.peek(); lts++) + model.validate(Query.selectPartition(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), false)); + } + catch (Throwable t) + { + // Unpause, since otherwise validation exception will prevent graceful shutdown + unpauseCommits(cmsInstance); + throw t; + } + + // Make sure there can be only one FinishJoin in flight + waitForCMSToQuiesce(cluster, cmsInstance); + // set expectation of finish join & retrieve the sequence when it gets committed + Callable bootstrapVisible = getSequenceAfterCommit(cmsInstance, (e, r) -> e instanceof PrepareJoin.FinishJoin && r.isSuccess()); + + // wait for the cluster to all witness the finish join event + unpauseCommits(cmsInstance); + waitForCMSToQuiesce(cluster, bootstrapVisible.call()); + + for (int i = 0; i < WRITES; i++) + visitor.visit(); + model.validateAll(); + } + } + + @Test + public void coordinatorIsBehindTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4)) + .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) + .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP).set("metadata_snapshot_frequency", 5)) + .start()) + { + IInvokableInstance cmsInstance = cluster.get(1); + waitForCMSToQuiesce(cluster, cmsInstance); + configBuilder.setSUT(() -> new InJvmSut(cluster, () -> 2, (t) -> false) { + public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, Object... bindings) + { + try + { + return super.execute(statement, cl, coordinator, bindings); + } + catch (Throwable t) + { + // Avoid retries + return new Object[][]{}; + } + } + }); + Run run = configBuilder.build().createRun(); + + cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace + + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL); + waitForCMSToQuiesce(cluster, cluster.get(1)); + + cluster.filters().verbs(Verb.TCM_REPLICATION.id, + Verb.TCM_FETCH_CMS_LOG_RSP.id, + Verb.TCM_FETCH_PEER_LOG_RSP.id, + Verb.TCM_CURRENT_EPOCH_REQ.id) + .to(2) + .drop() + .on(); + + Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new); + IInstanceConfig config = cluster.newInstanceConfig() + .set("auto_bootstrap", true) + .set(Constants.KEY_DTEST_FULL_STARTUP, true) + .set("progress_barrier_default_consistency_level", "NODE_LOCAL"); + IInvokableInstance newInstance = cluster.bootstrap(config); + + // Prime the CMS node to pause before the finish join event is committed + Callable pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.MidJoin); + long [] metricCounts = new long[4]; + for (int i = 1; i <= 4; i++) + metricCounts[i - 1] = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); + Thread thread = new Thread(() -> newInstance.startup()); + thread.start(); + pending.call(); + + boolean triggered = false; + long[] markers = new long[4]; + outer: + for (int i = 0; i < 20; i++) + { + for (int n = 0; n < 4; n++) + markers[n] = cluster.get(n + 1).logs().mark(); + + try + { + visitor.visit(); + } + catch (Throwable t) + { + // ignore + } + for (int n = 0; n < markers.length; n++) + { + if ((n + 1) == 2) // skip 2nd node + continue; + + if (!cluster.get(n + 1) + .logs() + .grep(markers[n], "Routing is correct, but coordinator needs to catch-up") + .getResult() + .isEmpty()) + { + triggered = true; + break outer; + } + } + } + Assert.assertTrue("Should have triggered routing exception on the replica", triggered); + boolean metricTriggered = false; + for (int i = 1; i <= 4; i++) + { + long prevMetric = metricCounts[i - 1]; + long newMetric = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount()); + if (newMetric - prevMetric > 0) + { + metricTriggered = true; + break; + } + } + Assert.assertTrue("Metric CoordinatorBehindRing should have been bumped by at least one replica", metricTriggered); + + cluster.filters().reset(); + unpauseCommits(cmsInstance); + thread.join(); + } + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java new file mode 100644 index 0000000000..923602b67d --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test.ring; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; + +import org.junit.Test; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.NodeToolResult; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.streaming.StreamSession; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.Startup; +import org.apache.cassandra.utils.CassandraVersion; +import org.apache.cassandra.utils.FBUtilities; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.distributed.test.ring.BootstrapTest.populate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DecommissionTest extends TestBaseImpl +{ + @Test + public void testResumableDecom() throws IOException + { + try (Cluster cluster = builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .withInstanceInitializer(BB::install) + .start()) + { + populate(cluster, 0, 100, 1, 2, ConsistencyLevel.QUORUM); + cluster.get(2).nodetoolResult("decommission", "--force").asserts().failure(); + cluster.get(2).nodetoolResult("decommission", "--force").asserts().success(); + } + } + + public static class BB + { + static void install(ClassLoader cl, int nodeNumber) + { + if (nodeNumber != 2) + return; + new ByteBuddy().rebase(StreamSession.class) + .method(named("startStreamingFiles")) + .intercept(MethodDelegation.to(BB.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + static AtomicBoolean first = new AtomicBoolean(); + + public static void startStreamingFiles(@Nullable StreamSession.PrepareDirection prepareDirection, @SuperCall Callable zuper) throws Exception + { + if (!first.get()) + { + first.set(true); + throw new RuntimeException("Triggering streaming error"); + } + zuper.call(); + } + } + + @Test + public void testDecomDirectoryMinMaxVersions() throws IOException { + try (Cluster cluster = builder().withNodes(3) + .start()) + { + cluster.get(3).nodetoolResult("decommission", "--force").asserts().success(); + + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + + ClusterMetadataService.instance().commit(new Startup(metadata.myNodeId(), + metadata.directory.getNodeAddresses(metadata.myNodeId()), + new NodeVersion(new CassandraVersion("6.0.0"), + NodeVersion.CURRENT_METADATA_VERSION))); + }); + + cluster.get(2).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new Startup(metadata.myNodeId(), + metadata.directory.getNodeAddresses(metadata.myNodeId()), + new NodeVersion(new CassandraVersion("5.0.0"), + NodeVersion.CURRENT_METADATA_VERSION))); + }); + + for (int i = 1; i <= 2; i++) + { + cluster.get(i).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + assertEquals(new CassandraVersion("5.0.0"), metadata.directory.clusterMinVersion.cassandraVersion); + assertEquals(new CassandraVersion("6.0.0"), metadata.directory.clusterMaxVersion.cassandraVersion); + assertTrue(metadata.directory.versions.containsValue(NodeVersion.CURRENT)); + }); + } + } + } + + @Test + public void testMixedVersionBlockDecom() throws IOException { + try (Cluster cluster = builder().withNodes(3) + .start()) + { + cluster.get(3).nodetoolResult("decommission", "--force").asserts().success(); + + // make node2 run V0: + cluster.get(2).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + + ClusterMetadataService.instance().commit(new Startup(metadata.myNodeId(), + metadata.directory.getNodeAddresses(metadata.myNodeId()), + new NodeVersion(new CassandraVersion("4.0.0"), + Version.V0))); + }); + + // make node1 run V1: + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + + ClusterMetadataService.instance().commit(new Startup(metadata.myNodeId(), + metadata.directory.getNodeAddresses(metadata.myNodeId()), + new NodeVersion(new CassandraVersion("6.0.0"), + NodeVersion.CURRENT_METADATA_VERSION))); + }); + + NodeToolResult res = cluster.get(2).nodetoolResult("decommission", "--force"); + res.asserts().failure(); + assertTrue(res.getStdout().contains("Upgrade in progress")); + cluster.get(2).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + + ClusterMetadataService.instance().commit(new Startup(metadata.myNodeId(), + metadata.directory.getNodeAddresses(metadata.myNodeId()), + new NodeVersion(new CassandraVersion("6.0.0"), + NodeVersion.CURRENT_METADATA_VERSION))); + }); + cluster.get(2).nodetoolResult("decommission", "--force").asserts().success(); + } + } + + @Test + public void testPeersPostDecom() throws IOException + { + try (Cluster cluster = builder().withNodes(4) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .start()) + { + populate(cluster, 0, 100, 1, 2, ConsistencyLevel.QUORUM); + cluster.get(3).nodetoolResult("decommission", "--force").asserts().success(); + + int[] remainingNodes = {1, 2, 4}; + Set expectedPeers = new HashSet<>(); + for (int i : remainingNodes) + expectedPeers.add(cluster.get(i).config().broadcastAddress().getAddress().toString()); + + // Decommission should remove from both the peers & peers_v2 system tables + for (int i : remainingNodes) + { + cluster.get(i).runOnInstance(() -> { + for (String table : new String[] {"peers", "peers_v2"}) + { + Set values = new HashSet<>(); + QueryProcessor.executeInternal(String.format("SELECT peer from system.%s;", table)) + .forEach(r -> values.add(r.getInetAddress("peer").toString())); + assertEquals(2, values.size()); + for (String e : expectedPeers) + if (!e.equals(FBUtilities.getJustBroadcastAddress().toString())) + assertTrue(values.contains(e)); + } + }); + } + } + } + + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/NodeNotInRingTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/NodeNotInRingTest.java index e973e33cd9..98bb076f35 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/NodeNotInRingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/NodeNotInRingTest.java @@ -24,7 +24,6 @@ import org.junit.Assert; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.action.GossipHelper; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.test.TestBaseImpl; @@ -52,7 +51,7 @@ public class NodeNotInRingTest extends TestBaseImpl .outbound() .drop() .on(); - cluster.run(GossipHelper.removeFromRing(cluster.get(3)), 1, 2); + cluster.get(3).runOnInstance(() -> StorageService.instance.decommission(true)); cluster.run(inst -> inst.runsOnInstance(() -> { Assert.assertEquals("There should be 2 remaining nodes in ring", 2, StorageService.instance.effectiveOwnershipWithPort(KEYSPACE).size()); diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/PendingWritesTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/PendingWritesTest.java deleted file mode 100644 index 0e0cf7616a..0000000000 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/PendingWritesTest.java +++ /dev/null @@ -1,109 +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.distributed.test.ring; - -import java.net.InetSocketAddress; -import java.time.Duration; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.impl.DistributedTestSnitch; -import org.apache.cassandra.distributed.shared.NetworkTopology; -import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.PendingRangeCalculatorService; -import org.apache.cassandra.service.StorageService; - -import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; -import static org.apache.cassandra.distributed.action.GossipHelper.bootstrap; -import static org.apache.cassandra.distributed.action.GossipHelper.disseminateGossipState; -import static org.apache.cassandra.distributed.action.GossipHelper.statusToBootstrap; -import static org.apache.cassandra.distributed.action.GossipHelper.withProperty; -import static org.apache.cassandra.distributed.api.Feature.GOSSIP; -import static org.apache.cassandra.distributed.api.Feature.NETWORK; - -public class PendingWritesTest extends TestBaseImpl -{ - @Test - public void testPendingWrites() throws Throwable - { - int originalNodeCount = 2; - int expandedNodeCount = originalNodeCount + 1; - - try (Cluster cluster = builder().withNodes(originalNodeCount) - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(expandedNodeCount)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP)) - .start()) - { - BootstrapTest.populate(cluster, 0, 100); - IInstanceConfig config = cluster.newInstanceConfig(); - IInvokableInstance newInstance = cluster.bootstrap(config); - withProperty(JOIN_RING, false, () -> newInstance.startup(cluster)); - - cluster.forEach(statusToBootstrap(newInstance)); - cluster.run(bootstrap(false, Duration.ofSeconds(60), Duration.ofSeconds(60)), newInstance.config().num()); - - cluster.get(1).acceptsOnInstance((InetSocketAddress ip) -> { - Set set = new HashSet<>(); - for (Map.Entry, EndpointsForRange.Builder> e : StorageService.instance.getTokenMetadata().getPendingRanges(KEYSPACE)) - { - set.addAll(e.getValue().build().endpoints()); - } - Assert.assertEquals(set.size(), 1); - Assert.assertTrue(String.format("%s should contain %s", set, ip), - set.contains(DistributedTestSnitch.toCassandraInetAddressAndPort(ip))); - }).accept(cluster.get(3).broadcastAddress()); - - BootstrapTest.populate(cluster, 100, 150); - - newInstance.nodetoolResult("join").asserts().success(); - - cluster.run(disseminateGossipState(newInstance),1, 2); - - cluster.run((instance) -> { - instance.runOnInstance(() -> { - PendingRangeCalculatorService.instance.update(); - PendingRangeCalculatorService.instance.blockUntilFinished(); - }); - }, 1, 2); - - cluster.get(1).acceptsOnInstance((InetSocketAddress ip) -> { - Set set = new HashSet<>(); - for (Map.Entry, EndpointsForRange.Builder> e : StorageService.instance.getTokenMetadata().getPendingRanges(KEYSPACE)) - set.addAll(e.getValue().build().endpoints()); - assert set.size() == 0 : set; - }).accept(cluster.get(3).broadcastAddress()); - - for (Map.Entry e : BootstrapTest.count(cluster).entrySet()) - Assert.assertEquals("Node " + e.getKey() + " has incorrect row state", e.getValue().longValue(), 150L); - } - } -} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/RangeVersioningTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/RangeVersioningTest.java new file mode 100644 index 0000000000..47a80124e2 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/RangeVersioningTest.java @@ -0,0 +1,72 @@ +/* + * 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.ring; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; + +public class RangeVersioningTest extends FuzzTestBase +{ + @Test + public void existingPlacementsUnchangedAfterAlter() throws IOException + { + try (Cluster cluster = builder().withNodes(4) + .createWithoutStarting()) + { + cluster.get(1).startup(); + cluster.coordinator(1).execute("CREATE KEYSPACE IF NOT EXISTS test_ks1 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};", ConsistencyLevel.QUORUM); + cluster.get(2).startup(); + cluster.coordinator(1).execute("CREATE KEYSPACE IF NOT EXISTS test_ks2 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};", ConsistencyLevel.QUORUM); + cluster.get(3).startup(); + cluster.coordinator(1).execute("CREATE KEYSPACE IF NOT EXISTS test_ks3 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};", ConsistencyLevel.QUORUM); + cluster.get(4).startup(); + cluster.coordinator(1).execute("CREATE KEYSPACE IF NOT EXISTS test_ks4 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 4};", ConsistencyLevel.QUORUM); + cluster.get(1).runOnInstance(() -> { + ClusterMetadata metadata = ClusterMetadata.current(); + Epoch previous = Epoch.EMPTY; + + for (int i = 1; i <= 4; i++) + { + Epoch smallestSeen = null; + for (VersionedEndpoints.ForRange fr : metadata.placements + .get(ReplicationParams.simple(i)) + .writes.replicaGroups().values()) + { + if (smallestSeen == null || fr.lastModified().isBefore(smallestSeen)) + smallestSeen = fr.lastModified(); + } + System.out.printf("Smallest seen for rf %d is %s%n", i, smallestSeen); + Assert.assertTrue(String.format("%s should have been before %s for RF %d", previous, smallestSeen, i), + previous.isBefore(smallestSeen)); + previous = smallestSeen; + } + }); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/ReadsDuringBootstrapTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/ReadsDuringBootstrapTest.java deleted file mode 100644 index ba2cdc520e..0000000000 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/ReadsDuringBootstrapTest.java +++ /dev/null @@ -1,115 +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.distributed.test.ring; - -import java.io.IOException; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Test; - -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.implementation.bind.annotation.FieldValue; -import net.bytebuddy.implementation.bind.annotation.SuperCall; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.api.TokenSupplier; -import org.apache.cassandra.distributed.shared.NetworkTopology; -import org.apache.cassandra.distributed.test.TestBaseImpl; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsForRange; - -import static net.bytebuddy.matcher.ElementMatchers.named; -import static org.apache.cassandra.distributed.api.Feature.GOSSIP; -import static org.apache.cassandra.distributed.api.Feature.NETWORK; - -public class ReadsDuringBootstrapTest extends TestBaseImpl -{ - - @Test - public void readsDuringBootstrapTest() throws IOException, ExecutionException, InterruptedException, TimeoutException - { - int originalNodeCount = 3; - int expandedNodeCount = originalNodeCount + 1; - ExecutorService es = Executors.newSingleThreadExecutor(); - try (Cluster cluster = builder().withNodes(originalNodeCount) - .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(expandedNodeCount)) - .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(expandedNodeCount, "dc0", "rack0")) - .withConfig(config -> config.with(NETWORK, GOSSIP) - .set("read_request_timeout", String.format("%dms", Integer.MAX_VALUE)) - .set("request_timeout", String.format("%dms", Integer.MAX_VALUE))) - .withInstanceInitializer(BB::install) - .start()) - { - String query = withKeyspace("SELECT * FROM %s.tbl WHERE id = ?"); - cluster.schemaChange(withKeyspace("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};")); - cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (id int PRIMARY KEY)")); - cluster.get(1).runOnInstance(() -> BB.block.set(true)); - Future read = es.submit(() -> cluster.coordinator(1).execute(query, ConsistencyLevel.QUORUM, 3)); - long mark = cluster.get(1).logs().mark(); - bootstrapAndJoinNode(cluster); - cluster.get(1).logs().watchFor(mark, "New node /127.0.0.4"); - cluster.get(1).runOnInstance(() -> BB.block.set(false)); - // populate cache - for (int i = 0; i < 10; i++) - cluster.coordinator(1).execute(query, ConsistencyLevel.QUORUM, i); - cluster.get(1).runOnInstance(() -> BB.latch.countDown()); - read.get(); - } - finally - { - es.shutdown(); - } - } - - public static class BB - { - public static final AtomicBoolean block = new AtomicBoolean(); - public static final CountDownLatch latch = new CountDownLatch(1); - - private static void install(ClassLoader cl, Integer instanceId) - { - if (instanceId != 1) - return; - new ByteBuddy().rebase(AbstractReplicationStrategy.class) - .method(named("getCachedReplicas")) - .intercept(MethodDelegation.to(BB.class)) - .make() - .load(cl, ClassLoadingStrategy.Default.INJECTION); - } - - public static EndpointsForRange getCachedReplicas(long ringVersion, Token t, - @FieldValue("keyspaceName") String keyspaceName, - @SuperCall Callable zuper) throws Exception - { - if (keyspaceName.equals(KEYSPACE) && block.get()) - latch.await(); - return zuper.call(); - } - } -} diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/StopProcessingExceptionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/StopProcessingExceptionTest.java new file mode 100644 index 0000000000..244589a7c5 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/StopProcessingExceptionTest.java @@ -0,0 +1,78 @@ +/* + * 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.ring; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.cql3.statements.schema.CreateKeyspaceStatement; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.log.FuzzTestBase; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.ClusterMetadata; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class StopProcessingExceptionTest extends FuzzTestBase +{ + @Test + public void stopProcessingExceptionTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(2) + .withInstanceInitializer(BBFailHelper::install) + .start()) + { + long mark = cluster.get(2).logs().mark(); + cluster.get(2).runOnInstance(() -> BBFailHelper.enabled.set(true)); + cluster.coordinator(1).execute("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", + ConsistencyLevel.ALL); + cluster.get(2).logs().watchFor(mark, "All subsequent epochs will be ignored"); + } + } + + public static class BBFailHelper + { + static void install(ClassLoader cl, int nodeNumber) + { + if (nodeNumber == 2) + new ByteBuddy().rebase(CreateKeyspaceStatement.class) + .method(named("apply")) + .intercept(MethodDelegation.to(BBFailHelper.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + + public static AtomicBoolean enabled = new AtomicBoolean(false); + + public static Keyspaces apply(ClusterMetadata metadata, @SuperCall Callable zuper) throws Exception + { + if (enabled.get()) + throw new RuntimeException(); + + return zuper.call(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/streaming/StreamCloseInMiddleTest.java b/test/distributed/org/apache/cassandra/distributed/test/streaming/StreamCloseInMiddleTest.java index f2772bc17f..9a7c27f7a4 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/streaming/StreamCloseInMiddleTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/streaming/StreamCloseInMiddleTest.java @@ -19,10 +19,10 @@ package org.apache.cassandra.distributed.test.streaming; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.concurrent.Callable; import java.util.stream.Collectors; +import org.junit.Assert; import org.junit.Test; import net.bytebuddy.ByteBuddy; @@ -45,6 +45,7 @@ import org.assertj.core.api.Assertions; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + public class StreamCloseInMiddleTest extends TestBaseImpl { @Test @@ -65,6 +66,7 @@ public class StreamCloseInMiddleTest extends TestBaseImpl .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3)) .withInstanceInitializer(BBHelper::install) .withConfig(c -> c.with(Feature.values()) + .set("auto_bootstrap", false) .set("stream_entire_sstables", zeroCopyStreaming) // when die, this will try to halt JVM, which is easier to validate in the test // other levels require checking state of the subsystems @@ -81,13 +83,15 @@ public class StreamCloseInMiddleTest extends TestBaseImpl // now bootstrap a new node; streaming will fail IInvokableInstance node3 = ClusterUtils.addInstance(cluster, cluster.get(1).config(), c -> c.set("auto_bootstrap", true)); - node3.startup(); - for (String line : Arrays.asList("Error while waiting on bootstrap to complete. Bootstrap will have to be restarted", // bootstrap failed - "Some data streaming failed. Use nodetool to check bootstrap state and resume")) // didn't join ring because bootstrap failed - Assertions.assertThat(node3.logs().grep(line).getResult()) - .hasSize(1); - - assertNoNodeShutdown(cluster); + try + { + node3.startup(); + } + catch (Exception e) + { + Assert.assertTrue(String.format("Message does not ontain an expected string: %s", e.getMessage()), + e.getMessage().contains("Did not finish joining the ring.")); + } } } @@ -178,7 +182,7 @@ public class StreamCloseInMiddleTest extends TestBaseImpl return false; } - public static void install(ClassLoader classLoader, Integer num) + public static void install(ClassLoader classLoader, int num) { new ByteBuddy().rebase(SequentialWriter.class) .method(named("writeDirectlyToChannel").and(takesArguments(1))) diff --git a/test/distributed/org/apache/cassandra/distributed/test/tcm/LogReplicationSmokeTest.java b/test/distributed/org/apache/cassandra/distributed/test/tcm/LogReplicationSmokeTest.java new file mode 100644 index 0000000000..3d0b9a1894 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/tcm/LogReplicationSmokeTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test.tcm; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.extensions.ExtensionValue; +import org.apache.cassandra.tcm.extensions.IntValue; +import org.apache.cassandra.tcm.transformations.CustomTransformation; + +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LogReplicationSmokeTest extends TestBaseImpl +{ + @Test + public void testRequestingPeerWatermarks() throws Throwable + { + try (Cluster cluster = builder().withNodes(3) + .withConfig(config -> config.with(GOSSIP).with(NETWORK)) + .start()) + { + init(cluster); + IInvokableInstance cmsNode = cluster.get(1); + ClusterUtils.waitForCMSToQuiesce(cluster, cmsNode); + Epoch initialEpoch = getConsistentEpoch(cluster); + + int initialVal = getConsistentValue(cluster); + assertEquals(-1, initialVal); + + Epoch expectedEpoch = initialEpoch.nextEpoch(); + final int expectedVal = new Random(System.nanoTime()).nextInt(); + cluster.get(3).runOnInstance(() -> { + ClusterMetadataService.instance().commit(CustomTransformation.make(expectedVal)); + }); + + ClusterUtils.waitForCMSToQuiesce(cluster, cmsNode); + Epoch currentEpoch = getConsistentEpoch(cluster); + assertTrue(currentEpoch.is(expectedEpoch)); + int currentVal = getConsistentValue(cluster); + assertEquals(expectedVal, currentVal); + } + } + + private int getConsistentValue(Cluster cluster) + { + Set values = new HashSet<>(); + cluster.forEach(inst -> values.add( inst.callOnInstance(() -> { + ExtensionValue v = ClusterMetadata.current().extensions.get(CustomTransformation.PokeInt.METADATA_KEY); + return v == null ? -1 : ((IntValue) v).getValue(); + }))); + assertEquals(1, values.size()); + return values.iterator().next(); + } + + private Epoch getConsistentEpoch(Cluster cluster) + { + Map epochs = getEpochsDirectly(cluster); + assertEquals(1, new HashSet<>(epochs.values()).size()); + return epochs.values().iterator().next(); + } + + private Map getEpochsDirectly(Cluster cluster) + { + Map epochs = new HashMap<>(); + cluster.forEach(inst -> epochs.put(inst.broadcastAddress().toString(), ClusterUtils.getClusterMetadataVersion(inst))); + return epochs; + } + +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/topology/DecommissionAvoidTimeouts.java b/test/distributed/org/apache/cassandra/distributed/test/topology/DecommissionAvoidTimeouts.java index 05206de73f..2047cddc58 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/topology/DecommissionAvoidTimeouts.java +++ b/test/distributed/org/apache/cassandra/distributed/test/topology/DecommissionAvoidTimeouts.java @@ -18,7 +18,6 @@ package org.apache.cassandra.distributed.test.topology; -import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -51,8 +50,6 @@ import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.distributed.util.Coordinators; import org.apache.cassandra.distributed.util.QueryResultUtil; -import org.apache.cassandra.distributed.util.byterewrite.StatusChangeListener; -import org.apache.cassandra.distributed.util.byterewrite.StatusChangeListener.Hooks; import org.apache.cassandra.distributed.util.byterewrite.Undead; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.WriteTimeoutException; @@ -62,17 +59,20 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.transformations.PrepareLeave; import org.apache.cassandra.utils.AssertionUtils; import org.apache.cassandra.utils.FBUtilities; import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; public abstract class DecommissionAvoidTimeouts extends TestBaseImpl { public static final int DECOM_NODE = 6; @Test - public void test() throws IOException + public void test() throws Exception { try (Cluster cluster = Cluster.build(8) .withRacks(2, 4) @@ -103,17 +103,14 @@ public abstract class DecommissionAvoidTimeouts extends TestBaseImpl toDecom.coordinator().execute("INSERT INTO " + table + "(pk) VALUES (?)", ConsistencyLevel.EACH_QUORUM, key); } + Callable pending = pauseBeforeCommit(cluster.get(1), (e) -> e instanceof PrepareLeave.StartLeave); CompletableFuture nodetool = CompletableFuture.runAsync(() -> toDecom.nodetoolResult("decommission").asserts().success()); - - Hooks statusHooks = StatusChangeListener.hooks(DECOM_NODE); - statusHooks.leaving.awaitAndEnter(); - // make sure all nodes see the severity change ClusterUtils.awaitGossipStateMatch(cluster, cluster.get(DECOM_NODE), ApplicationState.SEVERITY); - cluster.forEach(i -> i.runOnInstance(() -> ((DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch()).updateScores())); + pending.call(); + unpauseCommits(cluster.get(1)); - statusHooks.leave.await(); + cluster.forEach(i -> i.runOnInstance(() -> ((DynamicEndpointSnitch) DatabaseDescriptor.getEndpointSnitch()).updateScores())); cluster.filters().verbs(Verb.GOSSIP_DIGEST_SYN.id).drop(); - statusHooks.leave.enter(); nodetool.join(); @@ -192,16 +189,12 @@ public abstract class DecommissionAvoidTimeouts extends TestBaseImpl .method(named("sortedByProximity")).intercept(MethodDelegation.to(BB.class)) .make() .load(cl, ClassLoadingStrategy.Default.INJECTION); - - if (node != DECOM_NODE) return; - StatusChangeListener.install(cl, node, StatusChangeListener.Status.LEAVING, StatusChangeListener.Status.LEAVE); } @Override public void close() throws Exception { Undead.close(); - StatusChangeListener.close(); } public static > C sortedByProximity(final InetAddressAndPort address, C replicas, @SuperCall Callable real) throws Exception diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/BatchUpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/BatchUpgradeTest.java index 7ba12ff598..c62b28b8ac 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/BatchUpgradeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/BatchUpgradeTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.apache.cassandra.Util; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import static org.junit.Assert.assertEquals; @@ -33,6 +34,7 @@ public class BatchUpgradeTest extends UpgradeTestBase new TestCase() .nodes(2) .nodesToUpgrade(2) + .withConfig(c -> c.with(Feature.GOSSIP)) .upgradesToCurrentFrom(v40).setup((cluster) -> { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".users (" + "userid uuid PRIMARY KEY," + diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java new file mode 100644 index 0000000000..ab6955bcc7 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java @@ -0,0 +1,143 @@ +/* + * 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.upgrade; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.Test; + +import harry.core.Configuration; +import harry.ddl.SchemaSpec; +import harry.visitors.MutatingVisitor; +import harry.visitors.QueryLogger; +import harry.visitors.RandomPartitionValidator; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.harry.ClusterState; +import org.apache.cassandra.distributed.harry.ExistingClusterSUT; +import org.apache.cassandra.distributed.harry.FlaggedRunner; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static harry.core.Configuration.VisitorPoolConfiguration.pool; +import static harry.ddl.ColumnSpec.asciiType; +import static harry.ddl.ColumnSpec.ck; +import static harry.ddl.ColumnSpec.int64Type; +import static harry.ddl.ColumnSpec.pk; +import static harry.ddl.ColumnSpec.regularColumn; +import static harry.ddl.ColumnSpec.staticColumn; +import static java.util.Arrays.asList; +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.tcm.log.SystemKeyspaceStorage.NAME; +import static org.junit.Assert.assertEquals; + +public class ClusterMetadataUpgradeHarryTest extends UpgradeTestBase +{ + @Test + public void simpleUpgradeTest() throws Throwable + { + ExecutorService es = executorFactory().pooled("harry", 1); + Listener listener = new Listener(); + CountDownLatch stopLatch = CountDownLatch.newCountDownLatch(1); + AtomicReference> harryRunner = new AtomicReference<>(); + new UpgradeTestBase.TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .withUpgradeListener(listener) + .setup((cluster) -> { + SchemaSpec schema = new SchemaSpec("harry", "test_table", + asList(pk("pk1", asciiType), pk("pk1", int64Type)), + asList(ck("ck1", asciiType), ck("ck1", int64Type)), + asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)), + asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type))); + + Configuration config = Configuration.fromFile("conf/harry-example.yaml") + .unbuild() + .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3)) + .setSUT(new ExistingClusterSUT(cluster, listener)) + .build(); + + Future f = es.submit(() -> { + try + { + new FlaggedRunner(config.createRun(), + config, + asList(pool("Writer", 1, MutatingVisitor::new), + pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))), + stopLatch).run(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + }); + harryRunner.set(f); + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); + }) + .runAfterNodeUpgrade((cluster, node) -> { + Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); // make sure harry executes in mixed mode + }) + .runAfterClusterUpgrade((cluster) -> { + + // make sure we haven't persisted any events; + cluster.stream().forEach((i) -> { + Object[][] res = i.executeInternal(String.format("select * from %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, NAME)); + assertEquals(0, res.length); + }); + + cluster.get(1).nodetoolResult("initializecms").asserts().success(); + cluster.get(1).nodetoolResult("reconfigurecms", "datacenter1:3").asserts().success(); + cluster.schemaChange(withKeyspace("create table %s.xyz (id int primary key)")); + stopLatch.decrement(); + harryRunner.get().get(); + }).run(); + } + + + private static class Listener implements UpgradeListener, ClusterState + { + // only ever one node down here. + public final AtomicInteger downNode = new AtomicInteger(0); + @Override + public void shutdown(int i) + { + downNode.set(i); + } + + @Override + public void startup(int i) + { + downNode.set(0); + } + + @Override + public boolean isDown(int i) + { + return downNode.get() == i; + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeTest.java new file mode 100644 index 0000000000..61ca3ab901 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeTest.java @@ -0,0 +1,223 @@ +/* + * 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.upgrade; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.google.monitoring.runtime.instrumentation.common.util.concurrent.Uninterruptibles; +import org.apache.cassandra.distributed.Constants; +import org.apache.cassandra.distributed.UpgradeableCluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.SimpleQueryResult; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.HintsServiceMetrics; +import org.apache.cassandra.tcm.membership.NodeId; +import org.awaitility.Awaitility; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.psjava.util.AssertStatus.assertTrue; + +public class ClusterMetadataUpgradeTest extends UpgradeTestBase +{ + + @Test + public void simpleUpgradeTest() throws Throwable + { + new TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .setup((cluster) -> { + cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor':2}")); + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + }) + .runAfterClusterUpgrade((cluster) -> { + cluster.get(1).nodetoolResult("initializecms").asserts().success(); + cluster.forEach(i -> + { + // The cast is unpleasant, but safe to do so as the upgraded instance is running the current version. + assertFalse("node " + i.config().num() + " is still in MIGRATING STATE", + ClusterUtils.isMigrating((IInvokableInstance) i)); + }); + cluster.get(2).nodetoolResult("reconfigurecms", "--sync", "3").asserts().success(); + cluster.schemaChange(withKeyspace("create table %s.xyz (id int primary key)")); + cluster.forEach(i -> { + Object [][] res = i.executeInternal("select host_id from system.local"); + assertTrue(NodeId.isValidNodeId(UUID.fromString(res[0][0].toString()))); + }); + }).run(); + } + + @Test + public void upgradeWithHintsTest() throws Throwable + { + final int rowCount = 50; + new TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP)) + .upgradesToCurrentFrom(v41) + .setup((cluster) -> { + cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor':3}")); + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (k int, v int, PRIMARY KEY (k))"); + cluster.get(2).nodetoolResult("pausehandoff").asserts().success(); + + // insert some data while node1 is down so that hints are written + cluster.get(1).shutdown().get(); + for (int i = 0; i < rowCount; i++) + cluster.coordinator(2).execute("INSERT INTO " + KEYSPACE + ".tbl(k,v) VALUES (?, ?)", ConsistencyLevel.ANY, i, i); + cluster.get(2).flush(KEYSPACE); + cluster.get(3).flush(KEYSPACE); + cluster.get(1).startup(); + + // Check that none of the writes got to node1 + SimpleQueryResult rows = cluster.get(1).executeInternalWithResult("SELECT * FROM " + KEYSPACE + ".tbl"); + assertFalse(rows.hasNext()); + }) + .runAfterClusterUpgrade((cluster) -> { + Awaitility.waitAtMost(10, TimeUnit.SECONDS).until(() -> { + SimpleQueryResult rows = cluster.get(1).executeInternalWithResult("SELECT * FROM " + KEYSPACE + ".tbl"); + return rows.toObjectArrays().length == rowCount; + }); + + IInvokableInstance inst = (IInvokableInstance)cluster.get(2); + long hintsDelivered = inst.callOnInstance(() -> { + return (long)HintsServiceMetrics.hintsSucceeded.getCount(); + }); + assertEquals(rowCount, hintsDelivered); + }).run(); + } + + @Test + public void upgradeIgnoreHostsTest() throws Throwable + { + new TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .setup((cluster) -> { + cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor':2}")); + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + }) + .runAfterClusterUpgrade((cluster) -> { + // todo; isolate node 3 - actually shutting it down makes us throw exceptions when test finishes + cluster.filters().allVerbs().to(3).drop(); + cluster.filters().allVerbs().from(3).drop(); + cluster.get(1).nodetoolResult("initializecms").asserts().failure(); // node3 unreachable + cluster.get(1).nodetoolResult("initializecms", "--ignore", "127.0.0.1").asserts().failure(); // can't ignore localhost + cluster.get(1).nodetoolResult("initializecms", "--ignore", "127.0.0.3").asserts().success(); + }).run(); + } + + @Test + public void upgradeHostIdUpdateTest() throws Throwable + { + Map expectedUUIDs = new HashMap<>(); + new TestCase() + .nodes(3) + .nodesToUpgrade(1, 2, 3) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .setup((cluster) -> { + cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor':2}")); + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + expectedUUIDs.putAll(getHostIds(cluster, 1)); + for (UUID hostId : expectedUUIDs.values()) + assertFalse(NodeId.isValidNodeId(hostId)); + }) + .runAfterClusterUpgrade((cluster) -> { + for (int i = 1; i <= 3; i++) + assertEquals(expectedUUIDs, getHostIds(cluster, i)); + + cluster.get(1).nodetoolResult("initializecms").asserts().success(); + + Map postUpgradeUUIDs = new HashMap<>(getHostIds(cluster, 1)); + boolean found = false; + for (int i = 0; i < 20; i++) + { + if (postUpgradeUUIDs.values().stream().allMatch(NodeId::isValidNodeId)) + { + found = true; + break; + } + System.out.println("NOT ALL VALID: "+postUpgradeUUIDs); + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + postUpgradeUUIDs = new HashMap<>(getHostIds(cluster, 1)); + } + assertTrue(found); + + for (int i = 2; i <= 3; i++) + while(!getHostIds(cluster, i).equals(postUpgradeUUIDs)) + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + }).run(); + } + + @Test + public void testSingleNodeUpgrade() throws Throwable + { + new TestCase() + .nodes(1) + .nodesToUpgrade(1) + .withConfig((cfg) -> cfg.with(Feature.NETWORK, Feature.GOSSIP) + .set(Constants.KEY_DTEST_FULL_STARTUP, true)) + .upgradesToCurrentFrom(v41) + .setup((cluster) -> { + cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor':1}")); + cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); + }) + .runAfterClusterUpgrade((cluster) -> { + cluster.get(1).nodetoolResult("initializecms").asserts().success(); + // make sure we can execute transformations: + cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl with comment = 'hello123'")); + }).run(); + } + + private static Map getHostIds(UpgradeableCluster cluster, int instance) + { + String gossipInfo = cluster.get(instance).nodetoolResult("gossipinfo").getStdout(); + InetAddressAndPort host = null; + Map hostIds = new HashMap<>(); + for (String l : gossipInfo.split("\n")) + { + if (l.startsWith("/")) + host = InetAddressAndPort.getByNameUnchecked(l.replace("/", "")); + + if (l.contains("HOST_ID")) + { + String hostIdStr = l.split(":")[2]; + hostIds.put(host, UUID.fromString(hostIdStr)); + } + } + return hostIds; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageColumnDeleteTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageColumnDeleteTest.java index 2a2ece644f..520d077082 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageColumnDeleteTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageColumnDeleteTest.java @@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.upgrade; import org.junit.Test; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.AssertUtils.row; @@ -33,6 +34,7 @@ public class CompactStorageColumnDeleteTest extends UpgradeTestBase new TestCase() .nodes(2) .nodesToUpgrade(2) + .withConfig(c -> c.with(Feature.GOSSIP)) .upgradesToCurrentFrom(v40) .setup((cluster) -> { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH COMPACT STORAGE"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageHiddenColumnTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageHiddenColumnTest.java index 93f770c7fc..91b2b8137b 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageHiddenColumnTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageHiddenColumnTest.java @@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.upgrade; import org.junit.Test; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.AssertUtils.row; @@ -33,6 +34,7 @@ public class CompactStorageHiddenColumnTest extends UpgradeTestBase new TestCase() .nodes(2) .nodesToUpgrade(2) + .withConfig(c -> c.with(Feature.GOSSIP)) .upgradesToCurrentFrom(v40) .setup((cluster) -> { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, PRIMARY KEY (pk, ck)) WITH COMPACT STORAGE"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageImplicitNullInClusteringTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageImplicitNullInClusteringTest.java index 52b792e459..09f3976925 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageImplicitNullInClusteringTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStorageImplicitNullInClusteringTest.java @@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.upgrade; import org.junit.Test; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.AssertUtils.row; @@ -33,6 +34,7 @@ public class CompactStorageImplicitNullInClusteringTest extends UpgradeTestBase new TestCase() .nodes(2) .nodesToUpgrade(2) + .withConfig(c -> c.with(Feature.GOSSIP)) .upgradesToCurrentFrom(v40) .setup((cluster) -> { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck1 int, ck2 int, v int, PRIMARY KEY (pk, ck1, ck2)) WITH COMPACT STORAGE"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStoragePagingTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStoragePagingTest.java index 16431901c8..9298631a8d 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStoragePagingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactStoragePagingTest.java @@ -24,6 +24,7 @@ import org.junit.Assert; import org.junit.Test; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; public class CompactStoragePagingTest extends UpgradeTestBase { @@ -33,6 +34,7 @@ public class CompactStoragePagingTest extends UpgradeTestBase new TestCase() .nodes(2) .nodesToUpgrade(2) + .withConfig(c -> c.with(Feature.GOSSIP)) .upgradesToCurrentFrom(v40) .setup((cluster) -> { cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH COMPACT STORAGE"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactionHistorySystemTableUpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactionHistorySystemTableUpgradeTest.java index a7b23ce5ce..bd62bf7cf2 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/CompactionHistorySystemTableUpgradeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/CompactionHistorySystemTableUpgradeTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.tools.ToolRunner; import static org.apache.cassandra.db.compaction.CompactionHistoryTabularData.COMPACTION_TYPE_PROPERTY; @@ -41,6 +42,7 @@ public class CompactionHistorySystemTableUpgradeTest extends UpgradeTestBase // all upgrades from v40 to current, excluding v50 -> v51 .singleUpgradeToCurrentFrom(v40) .singleUpgradeToCurrentFrom(v41) + .withConfig(c -> c.with(Feature.GOSSIP)) .setup((cluster) -> { //create table cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tb (" + diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java index 9846264e56..c9872597bc 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/DropCompactStorageTest.java @@ -42,6 +42,7 @@ public class DropCompactStorageTest extends UpgradeTestBase cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck) VALUES (1,1)", ConsistencyLevel.ALL); }) .runAfterClusterUpgrade((cluster) -> { + cluster.get(1).nodetoolResult("initializecms").asserts().success(); cluster.schemaChange("ALTER TABLE " + KEYSPACE + ".tbl DROP COMPACT STORAGE"); assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.ALL), diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeBatchTestBase.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeBatchTestBase.java index 427eb613c0..d7696f73d5 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeBatchTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeBatchTestBase.java @@ -25,6 +25,7 @@ import com.vdurmont.semver4j.Semver; import org.apache.cassandra.distributed.UpgradeableCluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IMessageFilters; import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; @@ -57,6 +58,7 @@ public class MixedModeBatchTestBase extends UpgradeTestBase .nodes(3) .nodesToUpgrade(1, 2) .upgrades(from, to) + .withConfig(c -> c.with(Feature.GOSSIP)) .setup(cluster -> { cluster.schemaChange("CREATE KEYSPACE test_simple WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};"); cluster.schemaChange("CREATE TABLE test_simple.names (key int PRIMARY KEY, name text)"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeConsistencyV30Test.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeConsistencyV30Test.java index 5ee80151a6..ae19ccea3d 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeConsistencyV30Test.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeConsistencyV30Test.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.apache.cassandra.distributed.UpgradeableCluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IUpgradeableInstance; import static java.lang.String.format; @@ -53,7 +54,8 @@ public class MixedModeConsistencyV30Test extends UpgradeTestBase .nodesToUpgrade(1) .upgradesToCurrentFrom(v30) .withConfig(config -> config.set("read_request_timeout_in_ms", SECONDS.toMillis(30)) - .set("write_request_timeout_in_ms", SECONDS.toMillis(30))) + .set("write_request_timeout_in_ms", SECONDS.toMillis(30)) + .with(Feature.GOSSIP)) .setup(cluster -> { Tester.createTable(cluster); for (Tester tester : testers) diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeFrom3ReplicationTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeFrom3ReplicationTest.java index 69d3dbec71..21be424b3b 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeFrom3ReplicationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeFrom3ReplicationTest.java @@ -24,6 +24,7 @@ import java.util.List; import org.junit.Test; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; import static org.apache.cassandra.distributed.shared.AssertUtils.row; @@ -40,6 +41,7 @@ public class MixedModeFrom3ReplicationTest extends UpgradeTestBase .nodes(3) .nodesToUpgrade(1, 2) .upgradesToCurrentFrom(v30) + .withConfig(c -> c.with(Feature.GOSSIP)) .setup(cluster -> { cluster.schemaChange("CREATE KEYSPACE test_simple WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};"); cluster.schemaChange("CREATE TABLE test_simple.names (key int PRIMARY KEY, name text)"); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeIndexTestBase.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeIndexTestBase.java index 923f29abd4..1b1a656dac 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeIndexTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeIndexTestBase.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import com.vdurmont.semver4j.Semver; import org.apache.cassandra.distributed.UpgradeableCluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IUpgradeableInstance; import static java.lang.String.format; @@ -53,7 +54,8 @@ public class MixedModeIndexTestBase extends UpgradeTestBase .nodesToUpgrade(1) .upgradesToCurrentFrom(initial) .withConfig(config -> config.set("read_request_timeout_in_ms", SECONDS.toMillis(30)) - .set("write_request_timeout_in_ms", SECONDS.toMillis(30))) + .set("write_request_timeout_in_ms", SECONDS.toMillis(30)) + .with(Feature.GOSSIP)) .setup(cluster -> { for (Tester tester : testers) { diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeRepairTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeRepairTest.java index 6e22be5db2..a886d04764 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeRepairTest.java @@ -55,7 +55,7 @@ public class MixedModeRepairTest extends UpgradeTestBase .nodes(2) .nodesToUpgrade(UPGRADED_NODE) .upgradesToCurrentFrom(v40) - .withConfig(config -> config.with(NETWORK, GOSSIP)) + .withConfig(config -> config.with(NETWORK, GOSSIP).set("storage_compatibility_mode", "CASSANDRA_4")) .setup(cluster -> { cluster.schemaChange(CREATE_TABLE); cluster.setUncaughtExceptionsFilter(throwable -> throwable instanceof RejectedExecutionException); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeTTLOverflowUpgradeTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeTTLOverflowUpgradeTest.java index 43e013ff53..dbae49151e 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeTTLOverflowUpgradeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeTTLOverflowUpgradeTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.apache.cassandra.cql3.Attributes; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.distributed.UpgradeableCluster; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.StorageCompatibilityMode; @@ -122,6 +123,7 @@ public class MixedModeTTLOverflowUpgradeTest extends UpgradeTestBase // all upgrades from v40 to current, excluding v50 -> v51 .singleUpgradeToCurrentFrom(v40) .singleUpgradeToCurrentFrom(v41) + .withConfig(c -> c.with(Feature.GOSSIP).set("storage_compatibility_mode", "CASSANDRA_4")) .setup(cluster -> { cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int PRIMARY KEY, v int)")); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeWritetimeOrTTLTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeWritetimeOrTTLTest.java index 4c559c2eef..5979fae79b 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeWritetimeOrTTLTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/MixedModeWritetimeOrTTLTest.java @@ -23,6 +23,7 @@ import java.util.List; import org.junit.Test; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICoordinator; import org.assertj.core.api.Assertions; @@ -48,6 +49,7 @@ public class MixedModeWritetimeOrTTLTest extends UpgradeTestBase // all upgrades from v40 to current, excluding v50 -> v51 .singleUpgradeToCurrentFrom(v40) .singleUpgradeToCurrentFrom(v41) + .withConfig(c -> c.with(Feature.GOSSIP)) .setup(cluster -> { ICoordinator coordinator = cluster.coordinator(1); diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/Pre40MessageFilterTest.java b/test/distributed/org/apache/cassandra/distributed/upgrade/Pre40MessageFilterTest.java index 9030fdc342..83895e6671 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/Pre40MessageFilterTest.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/Pre40MessageFilterTest.java @@ -52,7 +52,7 @@ public class Pre40MessageFilterTest extends UpgradeTestBase @Test public void reserializePre40RequestPaxosWithoutNetworkTest() throws Throwable { - reserializePre40RequestPaxosTest(config -> {}); + reserializePre40RequestPaxosTest(config -> config.with(Feature.GOSSIP)); } @Test diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java index 5ee8780204..4b2d0ae374 100644 --- a/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/upgrade/UpgradeTestBase.java @@ -30,19 +30,18 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import com.vdurmont.semver4j.Semver; -import com.vdurmont.semver4j.Semver.SemverType; - import org.junit.After; import org.junit.Assume; import org.junit.BeforeClass; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.vdurmont.semver4j.Semver; +import com.vdurmont.semver4j.Semver.SemverType; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.distributed.UpgradeableCluster; +import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.shared.DistributedTestBase; @@ -51,10 +50,12 @@ import org.apache.cassandra.distributed.shared.Versions; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.SimpleGraph; +import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_GC_INSPECTOR; import static org.apache.cassandra.distributed.shared.Versions.Version; import static org.apache.cassandra.distributed.shared.Versions.find; import static org.apache.cassandra.utils.SimpleGraph.sortedVertices; +// checkstyle: suppress below 'blockSystemPropertyUsage' public class UpgradeTestBase extends DistributedTestBase { private static final Logger logger = LoggerFactory.getLogger(UpgradeTestBase.class); @@ -69,6 +70,8 @@ public class UpgradeTestBase extends DistributedTestBase public static void beforeClass() throws Throwable { ICluster.setup(); + SKIP_GC_INSPECTOR.setBoolean(true); + System.setProperty("sigar.nativeLogging", "false"); } @@ -91,6 +94,7 @@ public class UpgradeTestBase extends DistributedTestBase public static final Semver v3X = new Semver("3.11.0", SemverType.LOOSE); public static final Semver v40 = new Semver("4.0-alpha1", SemverType.LOOSE); public static final Semver v41 = new Semver("4.1-alpha1", SemverType.LOOSE); + public static final Semver v42 = new Semver("4.2-alpha1", SemverType.LOOSE); public static final Semver v50 = new Semver("5.0-alpha1", SemverType.LOOSE); public static final Semver v51 = new Semver("5.1-alpha1", SemverType.LOOSE); @@ -163,6 +167,18 @@ public class UpgradeTestBase extends DistributedTestBase private final Set nodesToUpgrade = new LinkedHashSet<>(); private Consumer configConsumer; private Consumer builderConsumer; + private UpgradeListener upgradeListener = new UpgradeListener() + { + @Override + public void shutdown(int i) + { + } + + @Override + public void startup(int i) + { + } + }; public TestCase() { @@ -320,6 +336,12 @@ public class UpgradeTestBase extends DistributedTestBase return this; } + public TestCase withUpgradeListener(UpgradeListener listener) + { + this.upgradeListener = listener; + return this; + } + public void run() throws Throwable { if (setup == null) @@ -356,11 +378,13 @@ public class UpgradeTestBase extends DistributedTestBase for (int n : nodesToUpgrade) { + upgradeListener.shutdown(n); cluster.get(n).shutdown().get(); triggerGC(); cluster.get(n).setVersion(nextVersion); runBeforeNodeRestart.run(cluster, n); cluster.get(n).startup(); + upgradeListener.startup(n); runAfterNodeUpgrade.run(cluster, n); } @@ -409,6 +433,7 @@ public class UpgradeTestBase extends DistributedTestBase { return new TestCase().nodes(nodes) .upgradesToCurrentFrom(v30) + .withConfig(c -> c.with(Feature.GOSSIP)) .nodesToUpgrade(toUpgrade); } @@ -439,4 +464,10 @@ public class UpgradeTestBase extends DistributedTestBase { return current == numNodes ? 1 : current + 1; } + + public interface UpgradeListener + { + void shutdown(int i); + void startup(int i); + } } diff --git a/test/distributed/org/apache/cassandra/distributed/util/byterewrite/StatusChangeListener.java b/test/distributed/org/apache/cassandra/distributed/util/byterewrite/StatusChangeListener.java deleted file mode 100644 index b6b8776224..0000000000 --- a/test/distributed/org/apache/cassandra/distributed/util/byterewrite/StatusChangeListener.java +++ /dev/null @@ -1,133 +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.distributed.util.byterewrite; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.implementation.bind.annotation.SuperCall; -import org.apache.cassandra.distributed.impl.InstanceIDDefiner; -import org.apache.cassandra.distributed.util.TwoWay; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.Shared; - -import static net.bytebuddy.matcher.ElementMatchers.named; - -public class StatusChangeListener -{ - public enum Status - { - LEAVING("startLeaving"), - LEAVE("leaveRing"); - - private final String method; - - Status(String method) - { - this.method = method; - } - } - - public static void install(ClassLoader cl, int node, Status first, Status... rest) - { - install(cl, node, EnumSet.of(first, rest)); - } - - public static void install(ClassLoader cl, int node, Set statuses) - { - if (statuses.isEmpty()) throw new IllegalStateException("Need a set of status to listen to"); - - State.hooks.put(node, new Hooks()); - DynamicType.Builder builder = new ByteBuddy().rebase(StorageService.class); - for (Status s : statuses) - builder = builder.method(named(s.method)).intercept(MethodDelegation.to(BB.class)); - builder.make().load(cl, ClassLoadingStrategy.Default.INJECTION); - } - - public static void close() - { - for (Hooks hook : State.hooks.values()) - hook.close(); - } - - public static Hooks hooks(int node) - { - return Objects.requireNonNull(State.hooks.get(node), "Unknown node" + node); - } - - @Shared - public static class State - { - public static final Map hooks = new ConcurrentHashMap<>(); - } - - @Shared - public static class Hooks implements AutoCloseable - { - public static final TwoWay leaving = new TwoWay(); - public static final TwoWay leave = new TwoWay(); - - @Override - public void close() - { - for (TwoWay condition: Arrays.asList(leaving, leave)) - condition.close(); - } - } - - public static class BB - { - private static volatile int NODE = -1; - - public static void startLeaving(@SuperCall Runnable zuper) - { - // see org.apache.cassandra.service.StorageService.startLeaving - hooks().leaving.enter(); - zuper.run(); - } - - public static void leaveRing(@SuperCall Runnable zuper) - { - // see org.apache.cassandra.service.StorageService.leaveRing - hooks().leave.enter(); - zuper.run(); - } - - private static Hooks hooks() - { - return State.hooks.get(node()); - } - - private static int node() - { - int node = NODE; - if (node == -1) - node = NODE = Integer.parseInt(InstanceIDDefiner.getInstanceId().replace("node", "")); - return node; - } - } -} diff --git a/test/long/org/apache/cassandra/cql3/CorruptionTest.java b/test/long/org/apache/cassandra/cql3/CorruptionTest.java index 0ef43a0991..b021407a1d 100644 --- a/test/long/org/apache/cassandra/cql3/CorruptionTest.java +++ b/test/long/org/apache/cassandra/cql3/CorruptionTest.java @@ -17,7 +17,6 @@ */ package org.apache.cassandra.cql3; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -26,20 +25,24 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.apache.cassandra.ServerTestUtils; -import org.apache.cassandra.io.util.File; - import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import com.datastax.driver.core.*; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; import com.datastax.driver.core.policies.LoggingRetryPolicy; import com.datastax.driver.core.policies.Policies; import com.datastax.driver.core.utils.Bytes; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.io.util.FileWriter; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileWriter; import org.apache.cassandra.service.EmbeddedCassandraService; public class CorruptionTest diff --git a/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java b/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java index 9531dd2c8d..575450a93f 100644 --- a/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java +++ b/test/long/org/apache/cassandra/db/commitlog/CommitLogStressTest.java @@ -219,7 +219,7 @@ public abstract class CommitLogStressTest verifySizes(commitLog); commitLog.discardCompletedSegments(Schema.instance.getTableMetadata("Keyspace1", "Standard1").id, - CommitLogPosition.NONE, discardedPos); + CommitLogPosition.NONE, discardedPos); threads.clear(); System.out.format("Discarded at %s\n", discardedPos); diff --git a/test/long/org/apache/cassandra/db/compaction/LongCompactionsTest.java b/test/long/org/apache/cassandra/db/compaction/LongCompactionsTest.java index ce72ae62a4..caf9a359a3 100644 --- a/test/long/org/apache/cassandra/db/compaction/LongCompactionsTest.java +++ b/test/long/org/apache/cassandra/db/compaction/LongCompactionsTest.java @@ -196,7 +196,7 @@ public class LongCompactionsTest // another compaction attempt will be launched in the background by // each completing compaction: not much we can do to control them here FBUtilities.waitOnFutures(compactions); - } while (CompactionManager.instance.getPendingTasks() > 0 || CompactionManager.instance.getActiveCompactions() > 0); + } while (CompactionManager.instance.hasOngoingOrPendingTasks()); if (cfs.getLiveSSTables().size() > 1) { diff --git a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java index ad18d9fd46..6569b54ae4 100644 --- a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java +++ b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyCQLTest.java @@ -32,7 +32,6 @@ import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.Hex; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_STRICT_LCS_CHECKS; @@ -80,7 +79,7 @@ public class LongLeveledCompactionStrategyCQLTest extends CQLTester while(currentTimeMillis() - start < TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)) { - StorageService.instance.getTokenMetadata().invalidateCachedRings(); +// StorageService.instance.getTokenMetadata().invalidateCachedRings(); Uninterruptibles.sleepUninterruptibly(r.nextInt(1000), TimeUnit.MILLISECONDS); } diff --git a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java index bbc1b577de..6c4a8ddb9b 100644 --- a/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java +++ b/test/long/org/apache/cassandra/db/compaction/LongLeveledCompactionStrategyTest.java @@ -18,24 +18,35 @@ package org.apache.cassandra.db.compaction; import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.google.common.collect.Lists; - -import org.apache.cassandra.db.lifecycle.SSTableSet; -import org.apache.cassandra.db.rows.UnfilteredRowIterator; -import org.apache.cassandra.io.sstable.ISSTableScanner; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.Util; import org.apache.cassandra.UpdateBuilder; -import org.apache.cassandra.db.*; +import org.apache.cassandra.Util; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.ActiveRepairService; diff --git a/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java b/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java index f2bbfa6a60..4e07430415 100644 --- a/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java +++ b/test/long/org/apache/cassandra/io/sstable/CQLSSTableWriterLongTest.java @@ -25,20 +25,10 @@ import java.util.Random; import com.google.common.io.Files; import org.apache.cassandra.io.util.File; -import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.service.StorageService; public class CQLSSTableWriterLongTest { - @BeforeClass - public static void setup() throws Exception - { - SchemaLoader.prepareServer(); - StorageService.instance.initServer(); - } - @Test public void testWideRow() throws Exception { diff --git a/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java b/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java index 2e27738efc..bc7eb3b17b 100644 --- a/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java +++ b/test/long/org/apache/cassandra/locator/DynamicEndpointSnitchLongTest.java @@ -24,6 +24,7 @@ import java.util.*; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.service.StorageService; @@ -37,6 +38,7 @@ public class DynamicEndpointSnitchLongTest static { DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); } @Test diff --git a/test/long/org/apache/cassandra/streaming/LongStreamingTest.java b/test/long/org/apache/cassandra/streaming/LongStreamingTest.java index 19d7709cdc..50baf912f3 100644 --- a/test/long/org/apache/cassandra/streaming/LongStreamingTest.java +++ b/test/long/org/apache/cassandra/streaming/LongStreamingTest.java @@ -23,12 +23,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import com.google.common.io.Files; + +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.io.util.File; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; @@ -53,8 +54,8 @@ public class LongStreamingTest public static void setup() throws Exception { DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); - SchemaLoader.cleanupAndLeaveDirs(); Keyspace.setInitialized(); StorageService.instance.initServer(); diff --git a/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java b/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java index d258026071..50691f851a 100644 --- a/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java +++ b/test/microbench/org/apache/cassandra/test/microbench/MutationBench.java @@ -22,14 +22,11 @@ package org.apache.cassandra.test.microbench; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collection; -import java.util.concurrent.*; +import java.util.concurrent.TimeUnit; import org.apache.cassandra.UpdateBuilder; -import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.io.util.DataInputBuffer; @@ -38,7 +35,20 @@ import org.apache.cassandra.io.util.DataOutputBufferFixed; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; -import org.openjdk.jmh.annotations.*; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.schema.TableMetadata; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.profile.StackProfiler; import org.openjdk.jmh.results.Result; import org.openjdk.jmh.results.RunResult; diff --git a/test/microbench/org/apache/cassandra/test/microbench/PendingRangesBench.java b/test/microbench/org/apache/cassandra/test/microbench/PendingRangesBench.java deleted file mode 100644 index e3644c70e6..0000000000 --- a/test/microbench/org/apache/cassandra/test/microbench/PendingRangesBench.java +++ /dev/null @@ -1,115 +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.test.microbench; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; -import org.apache.cassandra.dht.RandomPartitioner; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.PendingRangeMaps; -import org.apache.cassandra.locator.Replica; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; - -import java.net.UnknownHostException; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; - -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 50, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 3, jvmArgsAppend = "-Xmx512M") -@Threads(1) -@State(Scope.Benchmark) -public class PendingRangesBench -{ - PendingRangeMaps pendingRangeMaps; - int maxToken = 256 * 100; - - Multimap, Replica> oldPendingRanges; - - private Range genRange(String left, String right) - { - return new Range(new RandomPartitioner.BigIntegerToken(left), new RandomPartitioner.BigIntegerToken(right)); - } - - @Setup - public void setUp() throws UnknownHostException - { - pendingRangeMaps = new PendingRangeMaps(); - oldPendingRanges = HashMultimap.create(); - - List endpoints = Lists.newArrayList(InetAddressAndPort.getByName("127.0.0.1"), - InetAddressAndPort.getByName("127.0.0.2")); - - for (int i = 0; i < maxToken; i++) - { - for (int j = 0; j < ThreadLocalRandom.current().nextInt(2); j ++) - { - Range range = genRange(Integer.toString(i * 10 + 5), Integer.toString(i * 10 + 15)); - Replica replica = Replica.fullReplica(endpoints.get(j), range); - pendingRangeMaps.addPendingRange(range, replica); - oldPendingRanges.put(range, replica); - } - } - - // add the wrap around range - for (int j = 0; j < ThreadLocalRandom.current().nextInt(2); j ++) - { - Range range = genRange(Integer.toString(maxToken * 10 + 5), Integer.toString(5)); - Replica replica = Replica.fullReplica(endpoints.get(j), range); - pendingRangeMaps.addPendingRange(range, replica); - oldPendingRanges.put(range, replica); - } - } - - @Benchmark - public void searchToken(final Blackhole bh) - { - int randomToken = ThreadLocalRandom.current().nextInt(maxToken * 10 + 5); - Token searchToken = new RandomPartitioner.BigIntegerToken(Integer.toString(randomToken)); - bh.consume(pendingRangeMaps.pendingEndpointsFor(searchToken)); - } - - @Benchmark - public void searchTokenForOldPendingRanges(final Blackhole bh) - { - int randomToken = ThreadLocalRandom.current().nextInt(maxToken * 10 + 5); - Token searchToken = new RandomPartitioner.BigIntegerToken(Integer.toString(randomToken)); - Set replicas = new HashSet<>(); - for (Map.Entry, Collection> entry : oldPendingRanges.asMap().entrySet()) - { - if (entry.getKey().contains(searchToken)) - replicas.addAll(entry.getValue()); - } - bh.consume(replicas); - } - -} diff --git a/test/simulator/asm/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java b/test/simulator/asm/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java index fbea223339..67650639e0 100644 --- a/test/simulator/asm/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java +++ b/test/simulator/asm/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java @@ -73,6 +73,11 @@ class GlobalMethodTransformer extends MethodVisitor transformer.witness(GLOBAL_METHOD); super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", name, descriptor, false); } + else if (globalMethods && ((opcode == Opcodes.INVOKEVIRTUAL && owner.equals("java/util/concurrent/TimeUnit") && name.equals("sleep")))) + { + transformer.witness(GLOBAL_METHOD); + super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods$Global", name, "(Ljava/lang/Object;J)V", false); + } else if (globalMethods && ((opcode == Opcodes.INVOKESTATIC && ( owner.startsWith("org/apache/cassandra/utils/") && ( (owner.equals("org/apache/cassandra/utils/Clock") && name.equals("waitUntil")) @@ -81,8 +86,7 @@ class GlobalMethodTransformer extends MethodVisitor || owner.equals("java/util/UUID") && name.equals("randomUUID") || owner.equals("com/google/common/util/concurrent/Uninterruptibles") && name.equals("sleepUninterruptibly") || owner.equals("sun/misc/Unsafe") && name.equals("getUnsafe"))) - || (owner.equals("java/util/concurrent/TimeUnit") && name.equals("sleep"))) - ) + )) { transformer.witness(GLOBAL_METHOD); super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods$Global", name, descriptor, false); diff --git a/test/simulator/asm/org/apache/cassandra/simulator/asm/InterceptAgent.java b/test/simulator/asm/org/apache/cassandra/simulator/asm/InterceptAgent.java index c4beaf9c2b..4cf1546ca8 100644 --- a/test/simulator/asm/org/apache/cassandra/simulator/asm/InterceptAgent.java +++ b/test/simulator/asm/org/apache/cassandra/simulator/asm/InterceptAgent.java @@ -298,14 +298,12 @@ public class InterceptAgent visitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false); visitor.visitFieldInsn(GETSTATIC, "java/util/concurrent/ThreadLocalRandom", "SEED", "J"); visitor.visitMethodInsn(INVOKESTATIC, "org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods$Global", "randomSeed", "()J", false); - - String unsafeClass = Utils.descriptorToClassName(unsafeDescriptor); - visitor.visitMethodInsn(INVOKEVIRTUAL, unsafeClass, "putLong", "(Ljava/lang/Object;JJ)V", false); + visitor.visitMethodInsn(INVOKEVIRTUAL, unsafeDescriptor, "putLong", "(Ljava/lang/Object;JJ)V", false); visitor.visitFieldInsn(GETSTATIC, "java/util/concurrent/ThreadLocalRandom", unsafeFieldName, unsafeDescriptor); visitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false); visitor.visitFieldInsn(GETSTATIC, "java/util/concurrent/ThreadLocalRandom", "PROBE", "J"); visitor.visitLdcInsn(0); - visitor.visitMethodInsn(INVOKEVIRTUAL, unsafeClass, "putInt", "(Ljava/lang/Object;JI)V", false); + visitor.visitMethodInsn(INVOKEVIRTUAL, unsafeDescriptor, "putInt", "(Ljava/lang/Object;JI)V", false); visitor.visitInsn(RETURN); visitor.visitLabel(new Label()); visitor.visitMaxs(6, 1); diff --git a/test/simulator/bootstrap/org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods.java b/test/simulator/bootstrap/org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods.java index 645bbb82cb..a5c4c5adea 100644 --- a/test/simulator/bootstrap/org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods.java +++ b/test/simulator/bootstrap/org/apache/cassandra/simulator/systems/InterceptorOfSystemMethods.java @@ -38,6 +38,15 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; public interface InterceptorOfSystemMethods { void waitUntil(long deadlineNanos) throws InterruptedException; + default void sleep(long period) throws InterruptedException + { + sleepUninterriptibly(period, MILLISECONDS); + } + + default void sleep(Object unit, long period) throws InterruptedException + { + sleepUninterriptibly(period, (TimeUnit) unit); + } void sleep(long period, TimeUnit units) throws InterruptedException; void sleepUninterriptibly(long period, TimeUnit units); boolean waitUntil(Object monitor, long deadlineNanos) throws InterruptedException; @@ -83,9 +92,9 @@ public interface InterceptorOfSystemMethods } // slipped param order to replace instance method call without other ASM modification - public static void sleep(TimeUnit units, long period) throws InterruptedException + public static void sleep(Object units, long period) throws InterruptedException { - methods.sleep(period, units); + methods.sleep(period, (TimeUnit) units); } // to match Guava Uninterruptibles diff --git a/test/simulator/main/org/apache/cassandra/simulator/ActionList.java b/test/simulator/main/org/apache/cassandra/simulator/ActionList.java index 64474e6184..a6178c1870 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/ActionList.java +++ b/test/simulator/main/org/apache/cassandra/simulator/ActionList.java @@ -66,6 +66,11 @@ public class ActionList extends AbstractCollection return actions[i]; } + public Action asAction(Action.Modifiers self, Action.Modifiers children, String description) + { + return Actions.of(self, children, description, () -> this); + } + public Iterator iterator() { return Iterators.forArray(actions); @@ -118,6 +123,13 @@ public class ActionList extends AbstractCollection return this; } + public ActionList orderOn(OrderOn orderOn) + { + if (isEmpty()) return this; + forEach(a -> a.orderOn(orderOn)); + return this; + } + public Throwable safeForEach(Consumer forEach) { Throwable result = null; diff --git a/test/simulator/main/org/apache/cassandra/simulator/Actions.java b/test/simulator/main/org/apache/cassandra/simulator/Actions.java index eea1dd9116..4ddacb06a7 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/Actions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/Actions.java @@ -139,7 +139,9 @@ public class Actions public static Action stream(int concurrency, Supplier actions) { return stream(new OrderOn.Strict(actions, concurrency), actions); } public static Action stream(OrderOn on, Supplier actions) { return of(OrderOn.NONE, STREAM, NONE, on, () -> ActionList.of(streamNextSupplier(STREAM, STREAM_ITEM, on, 0, on, actions))); } public static Action infiniteStream(int concurrency, Supplier actions) { return infiniteStream(new OrderOn.Strict(actions, concurrency), actions); } - public static Action infiniteStream(OrderOn on, Supplier actions) { return of(OrderOn.NONE, INFINITE_STREAM, NONE, on, () -> ActionList.of(streamNextSupplier(INFINITE_STREAM, INFINITE_STREAM_ITEM, on, 0, on, actions))); } + public static Action infiniteStream(OrderOn on, Supplier actions) { return of(OrderOn.NONE, INFINITE_STREAM, NONE, on, () -> { + return ActionList.of(streamNextSupplier(INFINITE_STREAM, INFINITE_STREAM_ITEM, on, 0, on, actions)); + }); } private static ActionList next(Modifiers modifiers, Object description, int sequence, OrderOn on, Supplier actions) { Action next = actions.get(); @@ -151,7 +153,7 @@ public class Actions private static Action streamNextSupplier(Modifiers modifiers, Modifiers nextModifiers, Object description, int sequence, OrderOn on, Supplier actions) { return Actions.of(on, modifiers, NONE, - lazy(() -> description + " " + sequence), () -> next(nextModifiers, description, sequence, on, actions)); + lazy(() -> "infinite: " + description + " " + sequence), () -> next(nextModifiers, description, sequence, on, actions)); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java index 92e483487f..cbb83ceafc 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java @@ -23,6 +23,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -41,6 +42,7 @@ import com.google.common.util.concurrent.FutureCallback; import org.apache.cassandra.concurrent.ExecutorFactory; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.Constants; import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; @@ -54,6 +56,7 @@ import org.apache.cassandra.distributed.impl.DirectStreamingConnectionFactory; import org.apache.cassandra.distributed.impl.IsolatedExecutor; import org.apache.cassandra.io.compress.LZ4Compressor; import org.apache.cassandra.io.util.FileSystems; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.paxos.BallotGenerator; import org.apache.cassandra.service.paxos.PaxosPrepare; import org.apache.cassandra.simulator.RandomSource.Choices; @@ -183,11 +186,36 @@ public class ClusterSimulation implements AutoCloseable protected SchedulerFactory schedulerFactory = schedulerFactory(RunnableActionScheduler.Kind.values()); protected Debug debug = new Debug(); + protected Failures failures = new Failures(); protected Capture capture = new Capture(false, false, false); protected HeapPool.Logged.Listener memoryListener; protected SimulatedTime.Listener timeListener = (i1, i2) -> {}; protected LongConsumer onThreadLocalRandomCheck; + public Builder failures(Failures failures) + { + this.failures = failures; + return this; + } + + public Builder writeTimeoutNanos(long nanos) + { + this.writeTimeoutNanos = nanos; + return this; + } + + public Builder readTimeoutNanos(long nanos) + { + this.readTimeoutNanos = nanos; + return this; + } + + public Builder requestTimeoutNanos(long nanos) + { + this.requestTimeoutNanos = nanos; + return this; + } + public Debug debug() { return debug; @@ -450,7 +478,7 @@ public class ClusterSimulation implements AutoCloseable return this; } - public SimulatedFutureActionScheduler futureActionScheduler(int nodeCount, SimulatedTime time, RandomSource random) + public FutureActionScheduler futureActionScheduler(int nodeCount, SimulatedTime time, RandomSource random) { KindOfSequence kind = Choices.random(random, KindOfSequence.values()) .choose(random); @@ -461,6 +489,11 @@ public class ClusterSimulation implements AutoCloseable new SchedulerConfig(schedulerDelayChance, schedulerDelayNanos, schedulerLongDelayNanos)); } + public Map perVerbFutureActionSchedulers(int nodeCount, SimulatedTime time, RandomSource random) + { + return Collections.emptyMap(); + } + static SchedulerFactory schedulerFactory(RunnableActionScheduler.Kind... kinds) { return (random) -> { @@ -667,7 +700,7 @@ public class ClusterSimulation implements AutoCloseable InterceptAsClassTransformer interceptClasses = new InterceptAsClassTransformer(builder.monitorDelayChance.asSupplier(random), builder.nemesisChance.asSupplier(random), NemesisFieldSelectors.get(), ClassLoader.getSystemClassLoader(), sharedClassPredicate.negate()); threadLocalRandomCheck = new ThreadLocalRandomCheck(builder.onThreadLocalRandomCheck); - Failures failures = new Failures(); + Failures failures = builder.failures; ThreadAllocator threadAllocator = new ThreadAllocator(random, builder.threadCount, numOfNodes); List allowedDiskAccessModes = Arrays.asList("mmap", "mmap_index_only", "standard"); String disk_access_mode = allowedDiskAccessModes.get(random.uniform(0, allowedDiskAccessModes.size() - 1)); @@ -675,22 +708,21 @@ public class ClusterSimulation implements AutoCloseable cluster = snitch.setup(Cluster.build(numOfNodes) .withRoot(fs.getPath("/cassandra")) .withSharedClasses(sharedClassPredicate) - .withConfig(config -> { - config.with(Feature.BLANK_GOSSIP) - .set("read_request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.readTimeoutNanos))) - .set("write_request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.writeTimeoutNanos))) - .set("cas_contention_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.contentionTimeoutNanos))) - .set("request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.requestTimeoutNanos))) - .set("memtable_heap_space", "1MiB") - .set("memtable_allocation_type", builder.memoryListener != null ? "unslabbed_heap_buffers_logged" : "heap_buffers") - .set("file_cache_size", "16MiB") - .set("use_deterministic_table_id", true) - .set("disk_access_mode", disk_access_mode) - .set("failure_detector", SimulatedFailureDetector.Instance.class.getName()); - if (commitlogCompressed) - config.set("commitlog_compression", new ParameterizedClass(LZ4Compressor.class.getName(), emptyMap())); - configUpdater.accept(threadAllocator.update(config)); - }) + .withConfig(config -> configUpdater.accept(threadAllocator.update(config + .with(Feature.BLANK_GOSSIP) + .set(Constants.KEY_DTEST_JOIN_RING, false) + .set("read_request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.readTimeoutNanos))) + .set("write_request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.writeTimeoutNanos))) + .set("cas_contention_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.contentionTimeoutNanos))) + .set("request_timeout", String.format("%dms", NANOSECONDS.toMillis(builder.requestTimeoutNanos))) + .set("memtable_heap_space", "1MiB") + .set("memtable_allocation_type", builder.memoryListener != null ? "unslabbed_heap_buffers_logged" : "heap_buffers") + .set("file_cache_size", "16MiB") + .set("use_deterministic_table_id", true) + .set("disk_access_mode", "standard") + .set("failure_detector", SimulatedFailureDetector.Instance.class.getName()) + .set("commitlog_compression", new ParameterizedClass(LZ4Compressor.class.getName(), emptyMap())) + ))) .withInstanceInitializer(new IInstanceInitializer() { @Override @@ -763,9 +795,11 @@ public class ClusterSimulation implements AutoCloseable DirectStreamingConnectionFactory.setup(cluster); delivery = new SimulatedMessageDelivery(cluster); failureDetector = new SimulatedFailureDetector(cluster); - SimulatedFutureActionScheduler futureActionScheduler = builder.futureActionScheduler(numOfNodes, time, random); - simulated = new SimulatedSystems(random, time, delivery, execution, ballots, failureDetector, snitch, futureActionScheduler, builder.debug, failures); - simulated.register(futureActionScheduler); + FutureActionScheduler futureActionScheduler = builder.futureActionScheduler(numOfNodes, time, random); + Map perVerbFutureActionScheduler = builder.perVerbFutureActionSchedulers(numOfNodes, time, random); + simulated = new SimulatedSystems(random, time, delivery, execution, ballots, failureDetector, snitch, futureActionScheduler, perVerbFutureActionScheduler, builder.debug, failures); + if (futureActionScheduler instanceof SimulatedFutureActionScheduler) + simulated.register((SimulatedFutureActionScheduler) futureActionScheduler); RunnableActionScheduler scheduler = builder.schedulerFactory.create(random); ClusterActions.Options options = new ClusterActions.Options(builder.topologyChangeLimit, Choices.uniform(KindOfSequence.values()).choose(random).period(builder.topologyChangeIntervalNanos, random), diff --git a/test/simulator/main/org/apache/cassandra/simulator/Debug.java b/test/simulator/main/org/apache/cassandra/simulator/Debug.java index 8afdd3ed36..bcf0947fe4 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/Debug.java +++ b/test/simulator/main/org/apache/cassandra/simulator/Debug.java @@ -28,7 +28,6 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; - import javax.annotation.Nullable; import org.slf4j.Logger; @@ -48,19 +47,20 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.ReplicaLayout; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.simulator.systems.SimulatedTime; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import static java.util.function.Function.identity; import static org.apache.cassandra.simulator.Action.Modifier.INFO; import static org.apache.cassandra.simulator.Action.Modifier.WAKEUP; +import static org.apache.cassandra.simulator.ActionListener.recursive; import static org.apache.cassandra.simulator.ActionListener.runAfter; import static org.apache.cassandra.simulator.ActionListener.runAfterAndTransitivelyAfter; -import static org.apache.cassandra.simulator.ActionListener.recursive; -import static org.apache.cassandra.simulator.Debug.EventType.*; +import static org.apache.cassandra.simulator.Debug.EventType.CLUSTER; +import static org.apache.cassandra.simulator.Debug.EventType.PARTITION; import static org.apache.cassandra.simulator.Debug.Info.LOG; -import static org.apache.cassandra.simulator.Debug.Level.*; +import static org.apache.cassandra.simulator.Debug.Level.PLANNED; import static org.apache.cassandra.simulator.paxos.Ballots.paxosDebugInfo; // TODO (feature): move logging to a depth parameter @@ -350,7 +350,7 @@ public class Debug { return ignore -> cluster.forEach(i -> i.unsafeRunOnThisThread(() -> { if (Schema.instance.getKeyspaceMetadata(keyspace) != null) - logger.warn("{}", StorageService.instance.getTokenMetadata().toString()); + logger.warn("{}", ClusterMetadata.current()); })); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/Ordered.java b/test/simulator/main/org/apache/cassandra/simulator/Ordered.java index 3e57c95335..25dd4ebf87 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/Ordered.java +++ b/test/simulator/main/org/apache/cassandra/simulator/Ordered.java @@ -280,7 +280,9 @@ class Ordered extends OrderedLink implements ActionListener void invalidate(boolean isCancellation) { - Preconditions.checkState(!isCancellation || !isStarted); + if (isCancellation && isStarted) + new RuntimeException(String.format("Cancellation: %s. Started: %s", isCancellation, isStarted)).printStackTrace(); + //Preconditions.checkState(!isCancellation || !isStarted, String.format("Cancellation: %s. Started: %s", isCancellation, isStarted)); isStarted = isComplete = true; action.deregister(this); remove(); diff --git a/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java b/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java index 32888f4a11..07adf60572 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java +++ b/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java @@ -70,7 +70,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORE_MIS import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; import static org.apache.cassandra.config.CassandraRelevantProperties.LIBJEMALLOC; import static org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_OVERHEAD_SIZE; -import static org.apache.cassandra.config.CassandraRelevantProperties.MIGRATION_DELAY; import static org.apache.cassandra.config.CassandraRelevantProperties.PAXOS_REPAIR_RETRY_TIMEOUT_IN_MS; import static org.apache.cassandra.config.CassandraRelevantProperties.RING_DELAY; import static org.apache.cassandra.config.CassandraRelevantProperties.SHUTDOWN_ANNOUNCE_DELAY_IN_MS; @@ -124,7 +123,6 @@ public class SimulationRunner CONSISTENT_DIRECTORY_LISTINGS.setBoolean(true); TEST_IGNORE_SIGAR.setBoolean(true); SYSTEM_AUTH_DEFAULT_RF.setInt(3); - MIGRATION_DELAY.setInt(Integer.MAX_VALUE); DISABLE_GOSSIP_ENDPOINT_REMOVAL.setBoolean(true); MEMTABLE_OVERHEAD_SIZE.setInt(100); IGNORE_MISSING_NATIVE_FILE_HINTS.setBoolean(true); diff --git a/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java b/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java index 5be3384eeb..1cffb55a0b 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java +++ b/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java @@ -34,6 +34,15 @@ public class SimulatorUtils List oom = new ArrayList<>(); for (int i = 0 ; i < 1024 ; ++i) oom.add(new long[0x7fffffff]); + try + { + Thread.sleep(1_000); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + System.exit(1); throw new AssertionError(); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/ClusterActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/ClusterActions.java index ab66d50286..60b6a62e57 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/ClusterActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/ClusterActions.java @@ -21,6 +21,7 @@ package org.apache.cassandra.simulator.cluster; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -36,7 +37,9 @@ import org.apache.cassandra.config.Config.PaxosVariant; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.gms.Gossiper; @@ -45,25 +48,27 @@ import org.apache.cassandra.locator.ReplicaLayout; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; -import org.apache.cassandra.simulator.Actions.ReliableAction; +import org.apache.cassandra.simulator.Actions; import org.apache.cassandra.simulator.Actions.StrictAction; import org.apache.cassandra.simulator.Debug; import org.apache.cassandra.simulator.RandomSource.Choices; -import org.apache.cassandra.simulator.systems.InterceptedExecution; import org.apache.cassandra.simulator.systems.InterceptingExecutor; import org.apache.cassandra.simulator.systems.NonInterceptible; import org.apache.cassandra.simulator.systems.SimulatedSystems; import org.apache.cassandra.simulator.utils.KindOfSequence; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; -import static org.apache.cassandra.distributed.impl.UnsafeGossipHelper.addToRingNormalRunner; import static org.apache.cassandra.simulator.Action.Modifiers.NO_TIMEOUTS; import static org.apache.cassandra.simulator.Debug.EventType.CLUSTER; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.JOIN; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.LEAVE; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.REPLACE; +import static org.apache.cassandra.simulator.systems.InterceptedExecution.InterceptedRunnableExecution; import static org.apache.cassandra.simulator.systems.NonInterceptible.Permit.REQUIRED; import static org.apache.cassandra.simulator.utils.KindOfSequence.UNIFORM; - +import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; // TODO (feature): add Gossip failures (up to some acceptable number) // TODO (feature): add node down/up (need to coordinate bootstrap/repair execution around this) @@ -191,23 +196,51 @@ public class ClusterActions extends SimulatedSystems List actions = new ArrayList<>(); cluster.stream().forEach(i -> actions.add(invoke("Startup " + i.broadcastAddress(), NO_TIMEOUTS, NO_TIMEOUTS, - new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) i.executor(), i::startup)))); + new InterceptedRunnableExecution((InterceptingExecutor) i.executor(), i::startup)))); List endpoints = cluster.stream().map(IInstance::broadcastAddress).collect(Collectors.toList()); - cluster.forEach(i -> actions.add(resetGossipState(i, endpoints))); for (int add : joined) { - actions.add(transitivelyReliable("Add " + add + " to ring", cluster.get(add), addToRingNormalRunner(cluster.get(add)))); - actions.addAll(sendLocalGossipStateToAll(add)); + IInvokableInstance i = cluster.get(add); + actions.add(unsafeJoin(i)); } - actions.add(ReliableAction.transitively("Sync Pending Ranges Executor", ClusterActions.this::syncPendingRanges)); + actions.addAll(Quiesce.all(ClusterActions.this)); debug.debug(CLUSTER, time, cluster, null, null); return ActionList.of(actions); }); } + + public Action schemaChange(int node, String query) + { + String caption = String.format("Schema change: %s", query); + return new Actions.ReliableAction(caption, () -> { + List actions = new ArrayList<>(); + actions.add(new ClusterReliableQueryAction(caption, + ClusterActions.this, + node, + query, + ClusterActions.this.time.nextGlobalMonotonicMicros(), + ConsistencyLevel.ALL)); + actions.addAll(Quiesce.all(this)); + return ActionList.of(actions).setStrictlySequential(); + }, true); + } + + Action unsafeJoin(IInvokableInstance i) + { + return invoke("Initial cluster participant " + i.broadcastAddress(), NO_TIMEOUTS, NO_TIMEOUTS, + new InterceptedRunnableExecution((InterceptingExecutor) i.executor(), + () -> i.runOnInstance(() -> { + ClusterMetadataService.instance() + .commit(new UnsafeJoin(ClusterMetadata.current().myNodeId(), + new HashSet<>(BootStrapper.getBootstrapTokens(ClusterMetadata.current(), getBroadcastAddressAndPort())), + ClusterMetadataService.instance().placementProvider())); + }))); + } + Action resetGossipState(IInvokableInstance i, List endpoints) { return transitivelyReliable("Reset Gossip", i, () -> Gossiper.runInGossipStageBlocking(Gossiper.instance::unsafeSetEnabled)); @@ -238,7 +271,7 @@ public class ClusterActions extends SimulatedSystems Arrays.sort(vs1); Arrays.sort(vs2); if (!Arrays.equals(vs1, vs2)) - throw new AssertionError(); + throw new AssertionError(String.format("(from replicasForPrimaryKey) %s != %s (predicted)", Arrays.toString(vs1), Arrays.toString(vs2))); } } @@ -248,7 +281,7 @@ public class ClusterActions extends SimulatedSystems Keyspace keyspace = Keyspace.open(keyspaceName); TableMetadata metadata = keyspace.getColumnFamilyStore(table).metadata.get(); DecoratedKey key = metadata.partitioner.decorateKey(Int32Type.instance.decompose(primaryKey)); - // we return a Set because simulator can easily encounter point where nodes are both natural and pending + return ReplicaLayout.forTokenWriteLiveAndDown(keyspace, key.getToken()).all().asList(Replica::endpoint); } @@ -279,8 +312,6 @@ public class ClusterActions extends SimulatedSystems return on(action, IntStream.of(on)); } - ActionList syncPendingRanges() { return onAll(OnInstanceSyncPendingRanges.factory(this)); } - ActionList gossipWithAll(int from) { return toAll(OnInstanceGossipWith.factory(this), from); } ActionList sendShutdownToAll(int from) { return toAll(OnInstanceSendShutdown.factory(this), from); } ActionList sendLocalGossipStateToAll(int from) { return toAll(OnInstanceSendLocalGossipState.factory(this), from); } ActionList flushAndCleanup(int[] on) { return on(OnInstanceFlushAndCleanup.factory(this), on); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java index 55ab20a219..17edb9661b 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java @@ -18,26 +18,22 @@ package org.apache.cassandra.simulator.cluster; -import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.IntStream; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; -import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.NetworkTopologyStrategy; -import org.apache.cassandra.locator.PendingRangeMaps; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.distributed.test.log.PlacementSimulator; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.ActionListener; @@ -45,16 +41,18 @@ import org.apache.cassandra.simulator.ActionPlan; import org.apache.cassandra.simulator.Actions; import org.apache.cassandra.simulator.Debug; import org.apache.cassandra.simulator.OrderOn.StrictSequential; +import org.apache.cassandra.simulator.systems.InterceptedExecution; +import org.apache.cassandra.simulator.systems.InterceptingExecutor; import org.apache.cassandra.simulator.systems.SimulatedSystems; +import org.apache.cassandra.tcm.ClusterMetadataService; -import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.apache.cassandra.distributed.api.ConsistencyLevel.LOCAL_SERIAL; +import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; import static org.apache.cassandra.simulator.Debug.EventType.CLUSTER; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.CHANGE_RF; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.JOIN; import static org.apache.cassandra.simulator.cluster.ClusterActions.TopologyChange.LEAVE; -import static org.apache.cassandra.simulator.cluster.ClusterReliableQueryAction.schemaChange; public class KeyspaceActions extends ClusterActions { @@ -66,17 +64,17 @@ public class KeyspaceActions extends ClusterActions final EnumSet ops = EnumSet.noneOf(TopologyChange.class); final NodeLookup nodeLookup; + final PlacementSimulator.NodeFactory factory; final int[] minRf, initialRf, maxRf; final int[] membersOfQuorumDcs; // working state final NodesByDc all; - final NodesByDc prejoin; + final NodesByDc registered; final NodesByDc joined; final NodesByDc left; final int[] currentRf; - final TokenMetadata tokenMetadata = new TokenMetadata(snitch.get()); Topology topology; boolean haveChangedVariant; int topologyChangeCount = 0; @@ -99,18 +97,19 @@ public class KeyspaceActions extends ClusterActions this.nodeLookup = simulated.snitch; + this.factory = new PlacementSimulator.NodeFactory(new SimulationLookup()); int[] dcSizes = new int[options.initialRf.length]; for (int dc : nodeLookup.nodeToDc) ++dcSizes[dc]; this.all = new NodesByDc(nodeLookup, dcSizes); - this.prejoin = new NodesByDc(nodeLookup, dcSizes); + this.registered = new NodesByDc(nodeLookup, dcSizes); this.joined = new NodesByDc(nodeLookup, dcSizes); this.left = new NodesByDc(nodeLookup, dcSizes); for (int i = 1 ; i <= nodeLookup.nodeToDc.length ; ++i) { - this.prejoin.add(i); + this.registered.add(i); this.all.add(i); } @@ -148,19 +147,24 @@ public class KeyspaceActions extends ClusterActions { for (int i = 0 ; i < options.initialRf[dc] ; ++i) { - int join = prejoin.removeRandom(random, dc); + int join = registered.removeRandom(random, dc); joined.add(join); - tokenMetadata.updateNormalToken(tokenOf(join), inet(join)); } } updateTopology(recomputeTopology()); int[] joined = this.joined.toArray(); - int[] prejoin = this.prejoin.toArray(); + int[] prejoin = this.registered.toArray(); return Actions.StrictAction.of("Initialize", () -> { - return ActionList.of(initializeCluster(joined, prejoin), - schemaChange("Create Keyspace", KeyspaceActions.this, 1, createKeyspaceCql), - schemaChange("Create Table", KeyspaceActions.this, 1, createTableCql)); + List actions = new ArrayList<>(); + actions.add(initializeCluster(joined, prejoin)); + actions.add(schemaChange(1, createKeyspaceCql)); + actions.add(schemaChange(1, createTableCql)); + cluster.stream().forEach(i -> actions.add(invoke("Quiesce " + i.broadcastAddress(), RELIABLE_NO_TIMEOUTS, RELIABLE_NO_TIMEOUTS, + new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) i.executor(), + () -> i.runOnInstance(() -> ClusterMetadataService.instance().log().waitForHighestConsecutive()))))); + + return ActionList.of(actions); }); } @@ -179,14 +183,62 @@ public class KeyspaceActions extends ClusterActions })); } + private PlacementSimulator.ReplicatedRanges placements(NodesByDc nodesByDc, NodeLookup lookup, int[] rfs) + { + List nodes = new ArrayList<>(); + for (int dcIdx = 0; dcIdx < nodesByDc.dcs.length; dcIdx++) + { + int[] nodesInDc = nodesByDc.dcs[dcIdx]; + for (int i = 0; i < nodesByDc.dcSizes[dcIdx]; i++) + { + int nodeIdx = nodesInDc[i]; + PlacementSimulator.Node node = factory.make(nodeIdx,nodeIdx, 1); + nodes.add(node); + assert node.token() == tokenOf(nodeIdx); + } + } + + Map rf = new HashMap<>(); + for (int i = 0; i < rfs.length; i++) + rf.put(factory.lookup().dc(i + 1), rfs[i]); + + return new PlacementSimulator.NtsReplicationFactor(rfs).replicate(nodes); + } + + private Topology recomputeTopology(PlacementSimulator.ReplicatedRanges readPlacements, + PlacementSimulator.ReplicatedRanges writePlacements) + { + int[][] replicasForKey = new int[primaryKeys.length][]; + int[][] pendingReplicasForKey = new int[primaryKeys.length][]; + for (int i = 0 ; i < primaryKeys.length ; ++i) + { + int primaryKey = primaryKeys[i]; + LongToken token = new Murmur3Partitioner().getToken(Int32Type.instance.decompose(primaryKey)); + List readReplicas = readPlacements.replicasFor(token.token); + List writeReplicas = writePlacements.replicasFor(token.token); + + replicasForKey[i] = readReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); + Set pendingReplicas = new HashSet<>(writeReplicas); + pendingReplicas.removeAll(readReplicas); + replicasForKey[i] = readReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); + pendingReplicasForKey[i] = pendingReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); + } + + int[] membersOfRing = joined.toArray(); + long[] membersOfRingTokens = IntStream.of(membersOfRing).mapToLong(nodeLookup::tokenOf).toArray(); + + return new Topology(primaryKeys, membersOfRing, membersOfRingTokens, membersOfQuorum(), currentRf.clone(), + quorumRf(), replicasForKey, pendingReplicasForKey); + } + private Action next() { if (options.topologyChangeLimit >= 0 && topologyChangeCount++ > options.topologyChangeLimit) return null; - while (!ops.isEmpty() && (!prejoin.isEmpty() || joined.size() > sum(minRf))) + while (!ops.isEmpty() && (!registered.isEmpty() || joined.size() > sum(minRf))) { - if (options.changePaxosVariantTo != null && !haveChangedVariant && random.decide(1f / (1 + prejoin.size()))) + if (options.changePaxosVariantTo != null && !haveChangedVariant && random.decide(1f / (1 + registered.size()))) { haveChangedVariant = true; return schedule(new OnClusterSetPaxosVariant(KeyspaceActions.this, options.changePaxosVariantTo)); @@ -197,8 +249,8 @@ public class KeyspaceActions extends ClusterActions // try to pick an action (and simply loop again if we cannot for this dc) TopologyChange next; - if (prejoin.size(dc) > 0 && joined.size(dc) > currentRf[dc]) next = options.allChoices.choose(random); - else if (prejoin.size(dc) > 0 && ops.contains(JOIN)) next = options.choicesNoLeave.choose(random); + if (registered.size(dc) > 0 && joined.size(dc) > currentRf[dc]) next = options.allChoices.choose(random); + else if (registered.size(dc) > 0 && ops.contains(JOIN)) next = options.choicesNoLeave.choose(random); else if (joined.size(dc) > currentRf[dc] && ops.contains(LEAVE)) next = options.choicesNoJoin.choose(random); else if (joined.size(dc) > minRf[dc]) next = CHANGE_RF; else continue; @@ -206,57 +258,52 @@ public class KeyspaceActions extends ClusterActions // TODO (feature): introduce some time period between cluster actions switch (next) { + case JOIN: + { + Topology before = topology; + PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, nodeLookup, currentRf); + int join = registered.removeRandom(random, dc); + joined.add(join); + PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, nodeLookup, currentRf); + Topology during = recomputeTopology(placementsBefore, placementsAfter); + updateTopology(during); + Topology after = recomputeTopology(placementsAfter, placementsAfter); + Action action = new OnClusterJoin(KeyspaceActions.this, before, during, after, join); + return scheduleAndUpdateTopologyOnCompletion(action, after); + } case REPLACE: { Topology before = topology; - int join = prejoin.removeRandom(random, dc); + PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, nodeLookup, currentRf); + int join = registered.removeRandom(random, dc); int leave = joined.selectRandom(random, dc); joined.add(join); joined.remove(leave); left.add(leave); + PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, nodeLookup, currentRf); nodeLookup.setTokenOf(join, nodeLookup.tokenOf(leave)); - Collection token = singleton(tokenOf(leave)); - tokenMetadata.addReplaceTokens(token, inet(join), inet(leave)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology during = recomputeTopology(); + Topology during = recomputeTopology(placementsBefore, placementsAfter); updateTopology(during); - tokenMetadata.updateNormalTokens(token, inet(join)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology after = recomputeTopology(); - Action action = new OnClusterReplace(KeyspaceActions.this, before, during, after, leave, join); + Topology after = recomputeTopology(placementsAfter, placementsAfter); + Action action = null; + //TODO p + //new OnClusterReplace(KeyspaceActions.this, before, during, after, leave, join); return scheduleAndUpdateTopologyOnCompletion(action, after); // if replication factor is 2, cannot perform safe replacements // however can have operations that began earlier during RF=2 // so need to introduce some concept of barriers/ordering/sync points } - case JOIN: - { - Topology before = topology; - int join = prejoin.removeRandom(random, dc); - joined.add(join); - Collection token = singleton(tokenOf(join)); - tokenMetadata.addBootstrapTokens(token, inet(join)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology during = recomputeTopology(); - updateTopology(during); - tokenMetadata.updateNormalTokens(token, inet(join)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology after = recomputeTopology(); - Action action = new OnClusterJoin(KeyspaceActions.this, before, during, after, join); - return scheduleAndUpdateTopologyOnCompletion(action, after); - } + case LEAVE: { Topology before = topology; + PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, nodeLookup, currentRf); int leave = joined.removeRandom(random, dc); left.add(leave); - tokenMetadata.addLeavingEndpoint(inet(leave)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology during = recomputeTopology(); + PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, nodeLookup, currentRf); + Topology during = recomputeTopology(placementsBefore, placementsAfter); updateTopology(during); - tokenMetadata.removeEndpoint(inet(leave)); - tokenMetadata.unsafeCalculatePendingRanges(strategy(), keyspace); - Topology after = recomputeTopology(); + Topology after = recomputeTopology(placementsAfter, placementsAfter); Action action = new OnClusterLeave(KeyspaceActions.this, before, during, after, leave); return scheduleAndUpdateTopologyOnCompletion(action, after); } @@ -329,25 +376,8 @@ public class KeyspaceActions extends ClusterActions private Topology recomputeTopology() { - AbstractReplicationStrategy strategy = strategy(); - Map lookup = Cluster.getUniqueAddressLookup(cluster, i -> i.config().num()); - int[][] replicasForKey = new int[primaryKeys.length][]; - int[][] pendingReplicasForKey = new int[primaryKeys.length][]; - for (int i = 0 ; i < primaryKeys.length ; ++i) - { - int primaryKey = primaryKeys[i]; - Token token = new Murmur3Partitioner().getToken(Int32Type.instance.decompose(primaryKey)); - replicasForKey[i] = strategy.calculateNaturalReplicas(token, tokenMetadata) - .endpointList().stream().mapToInt(lookup::get).toArray(); - PendingRangeMaps pendingRanges = tokenMetadata.getPendingRanges(keyspace); - EndpointsForToken pendingEndpoints = pendingRanges == null ? null : pendingRanges.pendingEndpointsFor(token); - if (pendingEndpoints == null) pendingReplicasForKey[i] = new int[0]; - else pendingReplicasForKey[i] = pendingEndpoints.endpointList().stream().mapToInt(lookup::get).toArray(); - } - int[] membersOfRing = joined.toArray(); - long[] membersOfRingTokens = IntStream.of(membersOfRing).mapToLong(nodeLookup::tokenOf).toArray(); - return new Topology(primaryKeys, membersOfRing, membersOfRingTokens, membersOfQuorum(), currentRf.clone(), - quorumRf(), replicasForKey, pendingReplicasForKey); + PlacementSimulator.ReplicatedRanges ranges = placements(joined, nodeLookup, currentRf); + return recomputeTopology(ranges, ranges); } private int quorumRf() @@ -374,22 +404,34 @@ public class KeyspaceActions extends ClusterActions return sum; } - private InetAddressAndPort inet(int node) + private long tokenOf(int node) { - return InetAddressAndPort.getByAddress(cluster.get(node).config().broadcastAddress()); + return Long.parseLong(cluster.get(nodeLookup.tokenOf(node)).config().getString("initial_token")); } - AbstractReplicationStrategy strategy() + public class SimulationLookup extends PlacementSimulator.DefaultLookup { - Map rf = new HashMap<>(); - for (int i = 0 ; i < snitch.dcCount() ; ++i) - rf.put(snitch.nameOfDc(i), Integer.toString(currentRf[i])); - return new NetworkTopologyStrategy(keyspace, tokenMetadata, snitch.get(), rf); - } + public String dc(int dcIdx) + { + return super.dc(nodeLookup.dcOf(dcIdx) + 1); + } - private Token tokenOf(int node) - { - return new LongToken(Long.parseLong(cluster.get(nodeLookup.tokenOf(node)).config().getString("initial_token"))); - } + public String rack(int rackIdx) + { + return super.rack(1); + } + public long token(int tokenIdx) + { + return Long.parseLong(cluster.get(nodeLookup.tokenOf(tokenIdx)).config().getString("initial_token")); + } + + public PlacementSimulator.Lookup forceToken(int tokenIdx, long token) + { + SimulationLookup newLookup = new SimulationLookup(); + newLookup.overrides.putAll(overrides); + newLookup.overrides.put(tokenIdx, token); + return newLookup; + } + } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeRf.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeRf.java index 46067f9a90..afc8ce1283 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeRf.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeRf.java @@ -24,6 +24,7 @@ import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.Actions; import static org.apache.cassandra.simulator.Action.Modifiers.NONE; +import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; import static org.apache.cassandra.simulator.Action.Modifiers.STRICT; import static org.apache.cassandra.simulator.cluster.ClusterReliableQueryAction.schemaChange; import static org.apache.cassandra.utils.LazyToString.lazy; @@ -57,10 +58,10 @@ class OnClusterChangeRf extends OnClusterChangeTopology return ActionList.of( schemaChange("ALTER KEYSPACE " + description(), actions, on, command.toString()), - new OnClusterSyncPendingRanges(actions), new OnClusterFullRepair(actions, after, true, false, false), // TODO: cleanup should clear paxos state tables - Actions.of("Flush and Cleanup", !increase ? () -> actions.flushAndCleanup(after.membersOfRing) : ActionList::empty) + Actions.of("Flush and Cleanup", !increase ? () -> actions.flushAndCleanup(after.membersOfRing) : ActionList::empty), + Quiesce.all(actions).asAction(STRICT, RELIABLE_NO_TIMEOUTS, "Wait for cluster to quiesce") ); } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeTopology.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeTopology.java index 5f0d46ff50..ba44f8bbd7 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeTopology.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterChangeTopology.java @@ -24,7 +24,7 @@ import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.cluster.ClusterActionListener.TopologyChangeValidator; import org.apache.cassandra.simulator.systems.NonInterceptible; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE; +import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; import static org.apache.cassandra.simulator.Action.Modifiers.STRICT; import static org.apache.cassandra.simulator.ActionListener.runAfterTransitiveClosure; import static org.apache.cassandra.simulator.systems.NonInterceptible.Permit.REQUIRED; @@ -37,11 +37,12 @@ abstract class OnClusterChangeTopology extends Action implements Consumer actionList = new ArrayList<>(); + actionList.add(new SubmitPrepareJoin(actions, joining)); + actionList.add(new OnInstanceTopologyChangePaxosRepair(actions, joining, "Join")); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Start Join", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.START_JOIN)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS,"Mid Join", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.MID_JOIN)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS,"Finish Join", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.FINISH_JOIN)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + return ActionList.of(actionList); + } + + public static class SubmitPrepareJoin extends ClusterReliableAction + { + public SubmitPrepareJoin(ClusterActions actions, int on) + { + super("Prepare Join", actions, on, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new PrepareJoin(metadata.myNodeId(), + new HashSet<>(BootStrapper.getBootstrapTokens(metadata, FBUtilities.getBroadcastAddressAndPort())), + ClusterMetadataService.instance().placementProvider(), + true, + true)); + }); + } + } + + public static class ExecuteNextStep extends ClusterReliableAction + { + private ExecuteNextStep(ClusterActions actions, int on, Transformation.Kind kind) + { + this(actions, on, kind.ordinal()); + } + private ExecuteNextStep(ClusterActions actions, int on, int kind) + { + super(String.format("Execute next step of the join operation: %s", Transformation.Kind.values()[kind]), actions, on, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + MultiStepOperation sequence = metadata.inProgressSequences.get(metadata.myNodeId()); + + if (!(sequence instanceof BootstrapAndJoin)) + throw new IllegalStateException(String.format("Can not resume bootstrap as it does not appear to have been started. Found %s", sequence)); + + BootstrapAndJoin bootstrapAndJoin = ((BootstrapAndJoin) sequence); + assert bootstrapAndJoin.next.ordinal() == kind : String.format("Expected next step to be %s, but got %s", Transformation.Kind.values()[kind], bootstrapAndJoin.next); + boolean res = bootstrapAndJoin.finishJoiningRing().executeNext().isContinuable(); + assert res; + }); + } } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterLeave.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterLeave.java index 4250e243ee..d3d4ea6405 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterLeave.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterLeave.java @@ -18,16 +18,21 @@ package org.apache.cassandra.simulator.cluster; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; +import java.util.ArrayList; +import java.util.List; import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; -import org.apache.cassandra.simulator.systems.SimulatedActionConsumer; -import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.simulator.Actions; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.tcm.transformations.PrepareLeave; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; import static org.apache.cassandra.utils.LazyToString.lazy; class OnClusterLeave extends OnClusterChangeTopology @@ -42,19 +47,70 @@ class OnClusterLeave extends OnClusterChangeTopology public ActionList performSimple() { - IInvokableInstance leaveInstance = actions.cluster.get(leaving); - before(leaveInstance); - AtomicReference>> preparedUnbootstrap = new AtomicReference<>(); - return ActionList.of( - // setup the node's own gossip state for pending ownership, and return gossip actions to disseminate - new OnClusterUpdateGossip(actions, leaving, new OnInstanceSetLeaving(actions, leaving)), - new SimulatedActionConsumer<>("Prepare unbootstrap on " + leaving, RELIABLE_NO_TIMEOUTS, RELIABLE_NO_TIMEOUTS, actions, leaveInstance, - ref -> ref.set(StorageService.instance.prepareUnbootstrapStreaming()), preparedUnbootstrap), - new OnInstanceTopologyChangePaxosRepair(actions, leaving, "Leave"), - new SimulatedActionConsumer<>("Execute unbootstrap on " + leaving, RELIABLE_NO_TIMEOUTS, RELIABLE_NO_TIMEOUTS, actions, leaveInstance, - ref -> ref.get().get().syncThrowUncheckedOnInterrupt(), preparedUnbootstrap), - // setup the node's own gossip state for natural ownership, and return gossip actions to disseminate - new OnClusterUpdateGossip(actions, leaving, new OnInstanceSetLeft(actions, leaving)) - ); + IInvokableInstance joinInstance = actions.cluster.get(leaving); + before(joinInstance); + List actionList = new ArrayList<>(); + actionList.add(new SubmitPrepareLeave(actions, leaving)); + actionList.add(new OnInstanceTopologyChangePaxosRepair(actions, leaving, "Leave")); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Start Leave", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, leaving, Transformation.Kind.START_LEAVE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Mid Leave", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, leaving, Transformation.Kind.MID_LEAVE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Finish Leave", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, leaving, Transformation.Kind.FINISH_LEAVE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); + + return ActionList.of(actionList); + } + + public static class SubmitPrepareLeave extends ClusterReliableAction + { + public SubmitPrepareLeave(ClusterActions actions, int on) + { + super("Prepare Leave", actions, on, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new PrepareLeave(metadata.myNodeId(), + false, + ClusterMetadataService.instance().placementProvider(), + LeaveStreams.Kind.UNBOOTSTRAP)); + }); + } + } + + public static class ExecuteNextStep extends ClusterReliableAction + { + private ExecuteNextStep(ClusterActions actions, int on, Transformation.Kind kind) + { + this(actions, on, kind.ordinal()); + } + + private ExecuteNextStep(ClusterActions actions, int on, int kind) + { + super(String.format("Execute next step of the leave operation %s", Transformation.Kind.values()[kind]), actions, on, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + MultiStepOperation sequence = metadata.inProgressSequences.get(metadata.myNodeId()); + + if (!(sequence instanceof UnbootstrapAndLeave)) + throw new IllegalStateException(String.format("Can not resume leave as it does not appear to have been started. Found %s", sequence)); + + UnbootstrapAndLeave unbootstrapAndLeave = ((UnbootstrapAndLeave) sequence); + assert unbootstrapAndLeave.next.ordinal() == kind : String.format("Expected next step to be %s, but got %s", Transformation.Kind.values()[kind], unbootstrapAndLeave.next); + assert unbootstrapAndLeave.executeNext().isContinuable(); + }); + } } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterReplace.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterReplace.java index 8e0af4061f..8d93367c5f 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterReplace.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterReplace.java @@ -20,21 +20,27 @@ package org.apache.cassandra.simulator.cluster; import java.net.InetSocketAddress; import java.util.AbstractMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.stream.Collectors; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; -import org.apache.cassandra.simulator.Actions.ReliableAction; +import org.apache.cassandra.simulator.Actions; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.sequences.BootstrapAndReplace; +import org.apache.cassandra.tcm.transformations.PrepareReplace; import static org.apache.cassandra.simulator.Action.Modifiers.NONE; import static org.apache.cassandra.simulator.Action.Modifiers.STRICT; @@ -45,68 +51,117 @@ class OnClusterReplace extends OnClusterChangeTopology { final int leaving; final int joining; + final Topology during; OnClusterReplace(KeyspaceActions actions, Topology before, Topology during, Topology after, int leaving, int joining) { super(lazy(() -> String.format("node%d Replacing node%d", joining, leaving)), actions, STRICT, NONE, before, after, during.pendingKeys()); this.leaving = leaving; this.joining = joining; + this.during = during; } public ActionList performSimple() { - // need to mark it as DOWN, and perhaps shut it down - Map lookup = Cluster.getUniqueAddressLookup(actions.cluster); - IInvokableInstance leaveInstance = actions.cluster.get(leaving); IInvokableInstance joinInstance = actions.cluster.get(joining); - before(leaveInstance); - UUID hostId = leaveInstance.unsafeCallOnThisThread(SystemKeyspace::getLocalHostId); - String movingToken = leaveInstance.unsafeCallOnThisThread(() -> Utils.currentToken().toString()); - List> repairRanges = leaveInstance.unsafeApplyOnThisThread( - (String keyspaceName) -> StorageService.instance.getLocalAndPendingRanges(keyspaceName).stream() - .map(OnClusterReplace::toStringEntry) - .collect(Collectors.toList()), - actions.keyspace - ); + before(joinInstance); + int leavingNodeId = actions.cluster.get(leaving).unsafeCallOnThisThread(() -> ClusterMetadata.current().myNodeId().id()); + List actionList = new ArrayList<>(); + actionList.add(new SubmitPrepareReplace(actions, leavingNodeId, joining)); + actionList.add(new OnInstanceTopologyChangePaxosRepair(actions, joining, "Replace")); - int[] others = repairRanges.stream().mapToInt( - repairRange -> lookup.get(leaveInstance.unsafeApplyOnThisThread( - (String keyspaceName, String tk) -> Keyspace.open(keyspaceName).getReplicationStrategy().getNaturalReplicasForToken(Utils.parseToken(tk)).stream().map(Replica::endpoint) - .filter(i -> !i.equals(getBroadcastAddressAndPort())) - .findFirst() - .orElseThrow(IllegalStateException::new), - actions.keyspace, repairRange.getValue()) + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Start Replace", () -> { + List local = new ArrayList<>(); + + List> repairRanges = actions.cluster.get(leaving).unsafeApplyOnThisThread( + (String keyspaceName) -> { + ClusterMetadata metadata = ClusterMetadata.current(); + return metadata.placements.get(metadata.schema.getKeyspace(keyspaceName).getMetadata().params.replication) + .writes.ranges() + .stream() + .map(OnClusterReplace::toStringEntry) + .collect(Collectors.toList()); + }, actions.keyspace); + + + Map lookup = Cluster.getUniqueAddressLookup(actions.cluster); + + int[] others = repairRanges.stream().mapToInt( + repairRange -> lookup.get(actions.cluster.get(leaving).unsafeApplyOnThisThread( + (String keyspaceName, String tk) -> { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaces().getNullable(keyspaceName); + return metadata.placements.get(keyspaceMetadata.params.replication).reads + .forToken(Utils.parseToken(tk)) + .get() + .stream().map(Replica::endpoint) + .filter(i -> !i.equals(getBroadcastAddressAndPort())) + .findFirst() + .orElseThrow(IllegalStateException::new); + }, + actions.keyspace, repairRange.getValue()) ).config().num() - ).toArray(); + ).toArray(); - return ActionList.of( - // first sync gossip so that newly joined nodes are known by all, so that when we markdown we do not throw UnavailableException - ReliableAction.transitively("Sync Gossip", () -> actions.gossipWithAll(leaving)), + local.add(new OnClusterRepairRanges(actions, others, true, false, repairRanges)); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.START_REPLACE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); - // "shutdown" the leaving instance - new OnClusterUpdateGossip(actions, - ActionList.of(new OnInstanceMarkShutdown(actions, leaving), - new OnClusterMarkDown(actions, leaving)), - new OnInstanceSendShutdownToAll(actions, leaving)), + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Mid Replace", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.MID_REPLACE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); - // TODO (safety): confirm repair does not include this node + actionList.add(Actions.of(Modifiers.STRICT, Modifiers.RELIABLE_NO_TIMEOUTS, "Finish Replace", () -> { + List local = new ArrayList<>(); + local.add(new ExecuteNextStep(actions, joining, Transformation.Kind.FINISH_REPLACE)); + local.addAll(Quiesce.all(actions)); + return ActionList.of(local); + })); - // note that in the case of node replacement, we must perform a paxos repair before AND mid-transition. - // the first ensures the paxos state is flushed to the base table's sstables, so that the replacing node - // must receive a copy of all earlier operations (since the old node is now "offline") + return ActionList.of(actionList); + } - new OnClusterRepairRanges(actions, others, true, false, repairRanges), + private static class SubmitPrepareReplace extends ClusterReliableAction + { + public SubmitPrepareReplace(ClusterActions actions, int leavingNodeId, int joining) + { + super("Prepare Replace", actions, joining, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new PrepareReplace(new NodeId(leavingNodeId), + metadata.myNodeId(), + ClusterMetadataService.instance().placementProvider(), + true, + true)); + }); + } + } - // stream/repair from a peer - new OnClusterUpdateGossip(actions, joining, new OnInstanceSetBootstrapReplacing(actions, joining, leaving, hostId, movingToken)), + private static class ExecuteNextStep extends ClusterReliableAction + { + private ExecuteNextStep(ClusterActions actions, int on, Transformation.Kind kind) + { + this(actions, on, kind.ordinal()); + } - new OnInstanceSyncSchemaForBootstrap(actions, joining), - new OnInstanceTopologyChangePaxosRepair(actions, joining, "Replace"), - new OnInstanceBootstrap(actions, joinInstance, movingToken, true), + private ExecuteNextStep(ClusterActions actions, int on, int kind) + { + super(String.format("Execute next step of the replace operation: %s", Transformation.Kind.values()[kind]), actions, on, () -> { + ClusterMetadata metadata = ClusterMetadata.current(); + MultiStepOperation sequence = metadata.inProgressSequences.get(metadata.myNodeId()); - // setup the node's own gossip state for natural ownership, and return gossip actions to disseminate - new OnClusterUpdateGossip(actions, joining, new OnInstanceSetNormal(actions, joining, hostId, movingToken)) - ); + if (!(sequence instanceof BootstrapAndReplace)) + throw new IllegalStateException(String.format("Can not resume replace as it does not appear to have been started. Found: %s", sequence)); + + BootstrapAndReplace bootstrapAndReplace = ((BootstrapAndReplace) sequence); + assert bootstrapAndReplace.next.ordinal() == kind : String.format("Expected next step to be %s, but got %s", Transformation.Kind.values()[kind], bootstrapAndReplace.next); + assert bootstrapAndReplace.executeNext().isContinuable(); + }); + } } static Map.Entry toStringEntry(Range range) diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterUpdateGossip.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterUpdateGossip.java deleted file mode 100644 index 18263a3545..0000000000 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnClusterUpdateGossip.java +++ /dev/null @@ -1,64 +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.simulator.cluster; - -import org.apache.cassandra.simulator.Action; -import org.apache.cassandra.simulator.Actions.ReliableAction; -import org.apache.cassandra.simulator.ActionList; -import org.apache.cassandra.simulator.systems.SimulatedAction; - -import static org.apache.cassandra.simulator.Action.Modifier.DISPLAY_ORIGIN; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; -import static org.apache.cassandra.simulator.Action.Modifiers.STRICT; - -public class OnClusterUpdateGossip extends ReliableAction -{ - ActionList cancel; - OnClusterUpdateGossip(ClusterActions actions, int on, SimulatedAction updateLocalState) - { - this(updateLocalState.description() + " and Sync Gossip", actions, ActionList.of(updateLocalState), - new OnInstanceGossipWithAll(actions, on)); - } - - OnClusterUpdateGossip(ClusterActions actions, ActionList updateLocalState, Action sendGossip) - { - this(updateLocalState.get(0).description() + " and Sync Gossip", actions, updateLocalState, sendGossip); - } - - OnClusterUpdateGossip(Object id, ClusterActions actions, ActionList updateLocalState, Action sendGossip) - { - this(id, actions, updateLocalState.andThen(sendGossip)); - } - - OnClusterUpdateGossip(Object id, ClusterActions actions, ActionList updateLocalStateThenSendGossip) - { - super(id, STRICT.with(DISPLAY_ORIGIN), RELIABLE_NO_TIMEOUTS, () -> updateLocalStateThenSendGossip.andThen(new OnClusterSyncPendingRanges(actions))); - cancel = updateLocalStateThenSendGossip; - } - - @Override - protected Throwable safeInvalidate(boolean isCancellation) - { - ActionList list = cancel; - if (list == null) - return null; - cancel = null; - return list.safeForEach(Action::invalidate); - } -} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceBootstrap.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceBootstrap.java deleted file mode 100644 index 8fa9375a67..0000000000 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceBootstrap.java +++ /dev/null @@ -1,54 +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.simulator.cluster; - -import java.util.List; - -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.distributed.api.IIsolatedExecutor; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.simulator.systems.SimulatedActionTask; - -import static java.util.Collections.singletonList; -import static org.apache.cassandra.simulator.Action.Modifier.DISPLAY_ORIGIN; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; -import static org.apache.cassandra.simulator.cluster.Utils.parseTokens; - -class OnInstanceBootstrap extends SimulatedActionTask -{ - public OnInstanceBootstrap(ClusterActions actions, IInvokableInstance on) - { - this(actions, on, on.config().getString("initial_token"), false); - } - - public OnInstanceBootstrap(ClusterActions actions, IInvokableInstance on, String token, boolean replacing) - { - super("Bootstrap on " + on.config().num(), RELIABLE_NO_TIMEOUTS.with(DISPLAY_ORIGIN), RELIABLE_NO_TIMEOUTS, actions, on, - invokableBootstrap(token, replacing)); - } - - private static IIsolatedExecutor.SerializableRunnable invokableBootstrap(String token, boolean replacing) - { - return () -> { - List tokens = parseTokens(singletonList(token)); - StorageService.instance.startBootstrap(tokens, replacing); - }; - } -} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWith.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWith.java deleted file mode 100644 index 0532b35d89..0000000000 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceGossipWith.java +++ /dev/null @@ -1,50 +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.simulator.cluster; - -import java.net.InetSocketAddress; -import java.util.function.BiFunction; - -import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.IIsolatedExecutor; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.simulator.Action; - -import static org.apache.cassandra.locator.InetAddressAndPort.getByAddress; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; -import static org.apache.cassandra.simulator.Action.Modifiers.STRICT; - -class OnInstanceGossipWith extends ClusterAction -{ - OnInstanceGossipWith(ClusterActions actions, int from, int to) - { - super("Gossip from " + from + " to " + to, STRICT, RELIABLE_NO_TIMEOUTS, actions, from, invokableGossipWith(actions.cluster, to)); - } - - public static BiFunction factory(ClusterActions actions) - { - return (from, to) -> new OnInstanceGossipWith(actions, from, to); - } - - static IIsolatedExecutor.SerializableRunnable invokableGossipWith(Cluster cluster, int to) - { - InetSocketAddress address = cluster.get(to).broadcastAddress(); - return () -> Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.unsafeGossipWith(getByAddress(address))); - } -} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceRepair.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceRepair.java index 8344a65db7..f24e639ac3 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceRepair.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceRepair.java @@ -20,7 +20,6 @@ package org.apache.cassandra.simulator.cluster; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; import org.apache.cassandra.db.Keyspace; @@ -28,11 +27,12 @@ import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.api.IIsolatedExecutor; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.repair.RepairParallelism; import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.progress.ProgressEventType; @@ -86,9 +86,9 @@ class OnInstanceRepair extends ClusterAction private static void invokeRepair(String keyspaceName, boolean repairPaxos, boolean repairOnlyPaxos, boolean primaryRangeOnly, boolean force, Runnable listener) { Keyspace keyspace = Keyspace.open(keyspaceName); - TokenMetadata metadata = StorageService.instance.getTokenMetadata().cloneOnlyTokenMap(); + ClusterMetadata metadata = ClusterMetadata.current(); invokeRepair(keyspaceName, repairPaxos, repairOnlyPaxos, - () -> primaryRangeOnly ? Collections.singletonList(metadata.getPrimaryRangesFor(List.of(currentToken())).iterator().next()) + () -> primaryRangeOnly ? TokenRingUtils.getPrimaryRangesFor(metadata.tokenMap.tokens(), Collections.singleton(currentToken())) : keyspace.getReplicationStrategy().getAddressReplicas(metadata).get(getBroadcastAddressAndPort()).asList(Replica::range), primaryRangeOnly, force, listener); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncSchemaForBootstrap.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncSchemaForBootstrap.java deleted file mode 100644 index c8bc9f741e..0000000000 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncSchemaForBootstrap.java +++ /dev/null @@ -1,37 +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.simulator.cluster; - -import java.time.Duration; - -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.simulator.systems.SimulatedActionTask; - -import static org.apache.cassandra.simulator.Action.Modifier.DISPLAY_ORIGIN; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE_NO_TIMEOUTS; -import static org.junit.Assert.assertTrue; - -class OnInstanceSyncSchemaForBootstrap extends SimulatedActionTask -{ - public OnInstanceSyncSchemaForBootstrap(ClusterActions actions, int node) - { - super("Sync Schema on " + node, RELIABLE_NO_TIMEOUTS.with(DISPLAY_ORIGIN), RELIABLE_NO_TIMEOUTS, actions, actions.cluster.get(node), - () -> assertTrue("schema is ready", Schema.instance.waitUntilReady(Duration.ofMinutes(10)))); - } -} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncPendingRanges.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/Quiesce.java similarity index 53% rename from test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncPendingRanges.java rename to test/simulator/main/org/apache/cassandra/simulator/cluster/Quiesce.java index 4611d36fc2..24e9e316e4 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/OnInstanceSyncPendingRanges.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/Quiesce.java @@ -20,23 +20,36 @@ package org.apache.cassandra.simulator.cluster; import java.util.function.Function; -import org.apache.cassandra.service.PendingRangeCalculatorService; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; import org.apache.cassandra.simulator.Action; -import org.apache.cassandra.simulator.systems.SimulatedActionTask; +import org.apache.cassandra.simulator.ActionList; +import org.apache.cassandra.tcm.ClusterMetadataService; -import static org.apache.cassandra.simulator.Action.Modifiers.RELIABLE; - -class OnInstanceSyncPendingRanges extends SimulatedActionTask +/** + * Waits for a node to be fully caught up with the latest changes known to CMS + */ +public class Quiesce extends ClusterReliableAction { - OnInstanceSyncPendingRanges(ClusterActions actions, int node) + Quiesce(ClusterActions actions, int on) { - //noinspection Convert2MethodRef - invalid inspection across multiple classloaders - super("Sync Pending Ranges on " + node, RELIABLE, RELIABLE, actions, actions.cluster.get(node), - () -> PendingRangeCalculatorService.instance.blockUntilFinished()); + super("Quiesce " + on, actions, on, invokableQuiesce()); } - public static Function factory(ClusterActions clusterActions) + public static ActionList all(ClusterActions actions) { - return (node) -> new OnInstanceSyncPendingRanges(clusterActions, node); + return actions.onAll(Quiesce.factory(actions)); + } + + public static Function factory(ClusterActions actions) + { + return (on) -> new Quiesce(actions, on); + } + + private static IIsolatedExecutor.SerializableRunnable invokableQuiesce() + { + return () -> { + ClusterMetadataService.instance().processor().fetchLogAndWait(); + ClusterMetadataService.instance().log().waitForHighestConsecutive(); + }; } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/Utils.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/Utils.java index 277ae119e7..c3f463dfbe 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/Utils.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/Utils.java @@ -18,9 +18,6 @@ package org.apache.cassandra.simulator.cluster; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; @@ -28,11 +25,8 @@ import java.util.stream.Collectors; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.EndpointState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.TokenSerializer; -import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; import static org.apache.cassandra.config.DatabaseDescriptor.getPartitioner; import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; @@ -41,16 +35,9 @@ public class Utils { static Token currentToken() { - EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(getBroadcastAddressAndPort()); - VersionedValue value = epState.getApplicationState(ApplicationState.TOKENS); - try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(value.toBytes()))) - { - return TokenSerializer.deserialize(getPartitioner(), in).iterator().next(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId nodeId = metadata.directory.peerId(getBroadcastAddressAndPort()); + return metadata.tokenMap.tokens(nodeId).iterator().next(); } static List parseTokens(Collection tokens) diff --git a/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java b/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java new file mode 100644 index 0000000000..b3a3afc271 --- /dev/null +++ b/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java @@ -0,0 +1,147 @@ +/* + * 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.simulator.harry; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import harry.core.Run; +import harry.data.ResultSetRow; +import harry.model.Model; +import harry.model.SelectHelper; +import harry.model.sut.TokenPlacementModel; +import harry.model.sut.injvm.QuiescentLocalStateChecker; +import harry.operations.CompiledStatement; +import harry.operations.Query; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstance; +import org.apache.cassandra.simulator.SimulatorUtils; +import org.apache.cassandra.simulator.systems.InterceptedExecution; +import org.apache.cassandra.simulator.systems.InterceptingExecutor; +import org.apache.cassandra.simulator.systems.SimulatedAction; +import org.apache.cassandra.simulator.systems.SimulatedSystems; + +public class HarryValidatingQuery extends SimulatedAction +{ + private static final Logger logger = LoggerFactory.getLogger(HarryValidatingQuery.class); + private final Run run; + private final Query query; + private final InterceptingExecutor on; + private final Cluster cluster; + private final List owernship; + private final TokenPlacementModel.ReplicationFactor rf; + + public HarryValidatingQuery(SimulatedSystems simulated, + Cluster cluster, + TokenPlacementModel.ReplicationFactor rf, + Run run, + List owernship, + Query query) + { + super(query, Modifiers.RELIABLE_NO_TIMEOUTS, Modifiers.RELIABLE_NO_TIMEOUTS, null, simulated); + this.rf = rf; + this.cluster = cluster; + this.on = (InterceptingExecutor) cluster.get(1).executor(); + this.run = run; + this.query = query; + this.owernship = owernship; + } + + protected InterceptedExecution task() + { + return new InterceptedExecution.InterceptedTaskExecution(on) + { + public void run() + { + try + { + QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, rf) + { + @Override + protected TokenPlacementModel.ReplicatedRanges getRing() + { + return rf.replicate(owernship); + } + + @Override + protected void validate(Query query, TokenPlacementModel.ReplicatedRanges ring) + { + CompiledStatement compiled = query.toSelectStatement(); + List replicas = ring.replicasFor(this.token(query.pd)); + logger.trace("Predicted {} as replicas for {}. Ring: {}", new Object[]{ replicas, query.pd, ring }); + List throwables = new ArrayList<>(); + for (TokenPlacementModel.Node node : replicas) + { + try + { + validate(() -> { + Object[][] objects = this.executeNodeLocal(compiled.cql(), node, compiled.bindings()); + List result = new ArrayList(); + int length = objects.length; + for (int i = 0; i < length; ++i) + { + Object[] res = objects[i]; + result.add(SelectHelper.resultSetToRow(query.schemaSpec, this.clock, res)); + } + + return result; + }, query); + } + catch (Model.ValidationException t) + { + throwables.add(new AssertionError(String.format("Caught an exception while validating %s on %s", query, node), t)); + } + } + + if (!throwables.isEmpty()) + { + logger.error(String.format("Could not validate %d out of %d replicas %s", throwables.size(), replicas.size(), replicas), throwables.get(0)); + SimulatorUtils.failWithOOM(); + } + } + + + @Override + protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) + { + IInstance instance = cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().equals(node.id)) + .findFirst() + .get(); + return instance.executeInternal(statement, bindings); + } + }; + + model.validate(query); + + } + catch (Throwable t) + { + logger.error("Caught an exception while validating", t); + t.printStackTrace(); + } + finally { } + } + }; + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingAwaitable.java b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingAwaitable.java index ef4e24da07..0719cdd59d 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingAwaitable.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingAwaitable.java @@ -275,7 +275,11 @@ abstract class InterceptingAwaitable implements Awaitable Condition maybeIntercept(InterceptedWait.Kind kind, long waitNanos) { assert intercepted == null; - assert !inner.isSignalled(); + + // It is possible that by the time we call `await` on a signal, it will already have been + // signalled, so we do not have to intercept or wait here. + if (inner.isSignalled()) + return inner; InterceptibleThread thread = ifIntercepted(); if (thread == null) diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingExecutor.java b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingExecutor.java index 94e068c661..a9c0fb2e00 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingExecutor.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingExecutor.java @@ -68,6 +68,7 @@ import static org.apache.cassandra.simulator.systems.SimulatedExecution.callable import static org.apache.cassandra.simulator.systems.SimulatedTime.Global.localToRelativeNanos; import static org.apache.cassandra.simulator.systems.SimulatedTime.Global.localToGlobalNanos; import static org.apache.cassandra.simulator.systems.SimulatedTime.Global.relativeToGlobalNanos; +import static org.apache.cassandra.simulator.systems.SimulatedTime.Global.relativeToLocalNanos; import static org.apache.cassandra.utils.Shared.Recursive.INTERFACES; import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; @@ -778,7 +779,7 @@ public interface InterceptingExecutor extends OrderOn public ScheduledFuture scheduleTimeoutWithDelay(Runnable run, long delay, TimeUnit unit) { - return scheduleTimeoutAt(run, relativeToGlobalNanos(unit.toNanos(delay))); + return scheduleTimeoutAt(run, relativeToLocalNanos(unit.toNanos(delay))); } public ScheduledFuture scheduleAt(Runnable run, long deadlineNanos) diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java index 45a64f4241..abc71eae35 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java @@ -102,7 +102,10 @@ public class InterceptingGlobalMethods extends InterceptingMonitors implements I return null; if (!disabled) + { + logger.error("Caught a non-intercepted thread! " + thread, new RuntimeException()); throw failWithOOM(); + } return null; } diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingMonitors.java b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingMonitors.java index eab35de07a..3aabd18e5e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingMonitors.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/InterceptingMonitors.java @@ -687,7 +687,10 @@ public abstract class InterceptingMonitors implements InterceptorOfGlobalMethods { if (!thread.isIntercepting() && disabled) return; else if (!thread.isIntercepting()) - throw new AssertionError(); + { + throw new AssertionError("Thread " + thread + " is running but is not simulated"); + } + checkForDeadlock(thread, state.heldBy); InterceptedMonitorWait wait = new InterceptedMonitorWait(UNBOUNDED_WAIT, 0L, state, thread, captureWaitSite(thread)); diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedAction.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedAction.java index 551616ff60..0cf60dfb5e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedAction.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedAction.java @@ -25,11 +25,11 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; - import javax.annotation.Nullable; -import org.apache.cassandra.concurrent.ImmediateExecutor; + import com.google.common.base.Preconditions; +import org.apache.cassandra.concurrent.ImmediateExecutor; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IMessage; import org.apache.cassandra.exceptions.RequestFailureReason; @@ -40,6 +40,7 @@ import org.apache.cassandra.net.Verb; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.Actions; +import org.apache.cassandra.simulator.FutureActionScheduler; import org.apache.cassandra.simulator.FutureActionScheduler.Deliver; import org.apache.cassandra.simulator.OrderOn; import org.apache.cassandra.simulator.systems.InterceptedExecution.InterceptedRunnableExecution; @@ -53,23 +54,23 @@ import static org.apache.cassandra.simulator.Action.Modifiers.DROP; import static org.apache.cassandra.simulator.Action.Modifiers.NONE; import static org.apache.cassandra.simulator.Action.Modifiers.PSEUDO_ORPHAN; import static org.apache.cassandra.simulator.Action.Modifiers.START_DAEMON_TASK; -import static org.apache.cassandra.simulator.Action.Modifiers.START_SCHEDULED_TASK; import static org.apache.cassandra.simulator.Action.Modifiers.START_INFINITE_LOOP; +import static org.apache.cassandra.simulator.Action.Modifiers.START_SCHEDULED_TASK; import static org.apache.cassandra.simulator.Action.Modifiers.START_TASK; import static org.apache.cassandra.simulator.Action.Modifiers.START_THREAD; import static org.apache.cassandra.simulator.Action.Modifiers.START_TIMEOUT_TASK; import static org.apache.cassandra.simulator.Action.Modifiers.WAKE_UP_THREAD; +import static org.apache.cassandra.simulator.Debug.Info.LOG; import static org.apache.cassandra.simulator.FutureActionScheduler.Deliver.DELIVER; import static org.apache.cassandra.simulator.FutureActionScheduler.Deliver.DELIVER_AND_TIMEOUT; import static org.apache.cassandra.simulator.FutureActionScheduler.Deliver.FAILURE; +import static org.apache.cassandra.simulator.systems.InterceptedWait.Kind.UNBOUNDED_WAIT; import static org.apache.cassandra.simulator.systems.InterceptedWait.Trigger.SIGNAL; import static org.apache.cassandra.simulator.systems.InterceptedWait.Trigger.TIMEOUT; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.MESSAGE; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.REDUNDANT_MESSAGE_TIMEOUT; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.SCHEDULED_TIMEOUT; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.TASK; -import static org.apache.cassandra.simulator.Debug.Info.LOG; -import static org.apache.cassandra.simulator.systems.InterceptedWait.Kind.UNBOUNDED_WAIT; import static org.apache.cassandra.utils.LazyToString.lazy; import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; @@ -339,7 +340,9 @@ public abstract class SimulatedAction extends Action implements InterceptorOfCon long expiresAtNanos = simulated.time.get(fromNum).localToGlobalNanos(message.expiresAtNanos()); boolean isReliable = is(Modifier.RELIABLE) || self.is(Modifier.RELIABLE); - Deliver deliver = isReliable ? DELIVER : simulated.futureScheduler.shouldDeliver(fromNum, toNum); + + FutureActionScheduler childScheduler = simulated.perVerbFutureSchedulers.getOrDefault(Verb.fromId(message.verb()), simulated.futureScheduler); + Deliver deliver = isReliable ? DELIVER : childScheduler.shouldDeliver(fromNum, toNum); List actions = new ArrayList<>(deliver == DELIVER_AND_TIMEOUT ? 2 : 1); switch (deliver) @@ -354,7 +357,7 @@ public abstract class SimulatedAction extends Action implements InterceptorOfCon Object description = lazy(() -> String.format("%s(%d) from %s to %s", verb, message.id(), message.from(), to.broadcastAddress())); OrderOn orderOn = task.executor.orderAppliesAfterScheduling(); Action action = applyTo(description, MESSAGE, orderOn, self, verb, task); - long deadlineNanos = simulated.futureScheduler.messageDeadlineNanos(fromNum, toNum); + long deadlineNanos = childScheduler.messageDeadlineNanos(fromNum, toNum); if (deliver == DELIVER && deadlineNanos >= expiresAtNanos) { if (isReliable) deadlineNanos = verb.isResponse() ? expiresAtNanos : expiresAtNanos / 2; @@ -381,8 +384,11 @@ public abstract class SimulatedAction extends Action implements InterceptorOfCon notify = from; } boolean isTimeout = deliver != FAILURE; + Executor callbackExecutor = notify.executorFor(verb.id); + if (callbackExecutor instanceof ImmediateExecutor) + callbackExecutor = to.executor(); InterceptedExecution.InterceptedTaskExecution failTask = new InterceptedRunnableExecution( - (InterceptingExecutor) notify.executorFor(verb.id), + (InterceptingExecutor) callbackExecutor, () -> notify.unsafeApplyOnThisThread((socketAddress, id, innerIsTimeout) -> { InetAddressAndPort address = InetAddressAndPort.getByAddress(socketAddress); RequestCallbacks.CallbackInfo callback = instance().callbacks.remove(id, address); @@ -405,7 +411,7 @@ public abstract class SimulatedAction extends Action implements InterceptorOfCon { default: throw new AssertionError(); case FAILURE: - long deadlineNanos = simulated.futureScheduler.messageFailureNanos(toNum, fromNum); + long deadlineNanos = childScheduler.messageFailureNanos(toNum, fromNum); if (deadlineNanos < expiresAtNanos) { action.setDeadline(simulated.time, deadlineNanos); @@ -414,7 +420,7 @@ public abstract class SimulatedAction extends Action implements InterceptorOfCon case DELIVER_AND_TIMEOUT: case TIMEOUT: long expirationIntervalNanos = from.unsafeCallOnThisThread(RequestCallbacks::defaultExpirationInterval); - action.setDeadline(simulated.time, simulated.futureScheduler.messageTimeoutNanos(expiresAtNanos, expirationIntervalNanos)); + action.setDeadline(simulated.time, childScheduler.messageTimeoutNanos(expiresAtNanos, expirationIntervalNanos)); break; } actions.add(action); diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionCallable.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionCallable.java index 2dc3d6a9ab..1bff3d7769 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionCallable.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionCallable.java @@ -26,8 +26,8 @@ import org.apache.cassandra.simulator.systems.InterceptedExecution.InterceptedTa public abstract class SimulatedActionCallable extends SimulatedAction implements BiConsumer { - private final IInvokableInstance on; - private SerializableCallable execute; + protected final IInvokableInstance on; + protected SerializableCallable execute; public SimulatedActionCallable(Object description, Modifiers self, Modifiers children, SimulatedSystems simulated, IInvokableInstance on, SerializableCallable execute) { diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionTask.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionTask.java index 6607e19bab..6f35e28fe2 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionTask.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedActionTask.java @@ -27,6 +27,7 @@ import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableRunnab import org.apache.cassandra.net.Verb; import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.OrderOn; +import org.apache.cassandra.simulator.OrderOns; import org.apache.cassandra.simulator.systems.InterceptedExecution.InterceptedTaskExecution; import org.apache.cassandra.utils.Throwables; @@ -55,6 +56,13 @@ public class SimulatedActionTask extends SimulatedAction implements Runnable task.onCancel(this); } + public SimulatedActionTask(Object description, Modifiers self, Modifiers children, SimulatedSystems simulated, Map verbModifiers, IInvokableInstance on, SerializableRunnable run) + { + super(description, TASK, OrderOns.NONE, self, children, verbModifiers, null, simulated); + this.task = unsafeAsTask(on, asSafeRunnable(on, run), simulated.failures); + task.onCancel(this); + } + private SimulatedActionTask(Object description, Modifiers self, Modifiers children, SimulatedSystems simulated, IInvokableInstance on, InterceptedExecution task) { super(description, self, children, null, simulated); diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedFailureDetector.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedFailureDetector.java index b473cfca4d..6bdc74c1d6 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedFailureDetector.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedFailureDetector.java @@ -118,6 +118,12 @@ public class SimulatedFailureDetector ); } + public void markUp(InetSocketAddress address) + { + override.put(address, Boolean.TRUE); + listeners.forEach(c -> c.accept(address)); + } + public void markDown(InetSocketAddress address) { override.put(address, Boolean.FALSE); diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSnitch.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSnitch.java index a87a88a2c5..4000946a94 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSnitch.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSnitch.java @@ -19,7 +19,9 @@ package org.apache.cassandra.simulator.systems; import java.net.InetSocketAddress; +import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.IntStream; @@ -118,6 +120,11 @@ public class SimulatedSnitch extends NodeLookup Instance.setup(lookup); } + public List dcs() + { + return Arrays.asList(nameOfDcs); + } + private static int asInt(Replica address) { byte[] bytes = address.endpoint().addressBytes; diff --git a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSystems.java b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSystems.java index c3cf46a5d8..ac63d11a71 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSystems.java +++ b/test/simulator/main/org/apache/cassandra/simulator/systems/SimulatedSystems.java @@ -20,9 +20,11 @@ package org.apache.cassandra.simulator.systems; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableRunnable; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.Action.Modifiers; import org.apache.cassandra.simulator.Debug; @@ -44,21 +46,22 @@ public class SimulatedSystems public final SimulatedFailureDetector failureDetector; public final SimulatedSnitch snitch; public final FutureActionScheduler futureScheduler; + public final Map perVerbFutureSchedulers; public final Debug debug; public final Failures failures; private final List topologyListeners; // TODO (cleanup): this is a mutable set of listeners but shared between instances public SimulatedSystems(SimulatedSystems copy) { - this(copy.random, copy.time, copy.delivery, copy.execution, copy.ballots, copy.failureDetector, copy.snitch, copy.futureScheduler, copy.debug, copy.failures, copy.topologyListeners); + this(copy.random, copy.time, copy.delivery, copy.execution, copy.ballots, copy.failureDetector, copy.snitch, copy.futureScheduler, copy.perVerbFutureSchedulers, copy.debug, copy.failures, copy.topologyListeners); } - public SimulatedSystems(RandomSource random, SimulatedTime time, SimulatedMessageDelivery delivery, SimulatedExecution execution, SimulatedBallots ballots, SimulatedFailureDetector failureDetector, SimulatedSnitch snitch, FutureActionScheduler futureScheduler, Debug debug, Failures failures) + public SimulatedSystems(RandomSource random, SimulatedTime time, SimulatedMessageDelivery delivery, SimulatedExecution execution, SimulatedBallots ballots, SimulatedFailureDetector failureDetector, SimulatedSnitch snitch, FutureActionScheduler defaultFutureScheduler, Map perVerbFutureScheduler, Debug debug, Failures failures) { - this(random, time, delivery, execution, ballots, failureDetector, snitch, futureScheduler, debug, failures, new ArrayList<>()); + this(random, time, delivery, execution, ballots, failureDetector, snitch, defaultFutureScheduler, perVerbFutureScheduler, debug, failures, new ArrayList<>()); } - private SimulatedSystems(RandomSource random, SimulatedTime time, SimulatedMessageDelivery delivery, SimulatedExecution execution, SimulatedBallots ballots, SimulatedFailureDetector failureDetector, SimulatedSnitch snitch, FutureActionScheduler futureScheduler, Debug debug, Failures failures, List topologyListeners) + protected SimulatedSystems(RandomSource random, SimulatedTime time, SimulatedMessageDelivery delivery, SimulatedExecution execution, SimulatedBallots ballots, SimulatedFailureDetector failureDetector, SimulatedSnitch snitch, FutureActionScheduler futureScheduler, Map perVerbFutureScheduler, Debug debug, Failures failures, List topologyListeners) { this.random = random; this.time = time; @@ -66,6 +69,7 @@ public class SimulatedSystems this.execution = execution; this.ballots = ballots; this.failureDetector = failureDetector; + this.perVerbFutureSchedulers = perVerbFutureScheduler; this.snitch = snitch; this.futureScheduler = futureScheduler; this.debug = debug; diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/AlwaysDeliverNetworkScheduler.java b/test/simulator/test/org/apache/cassandra/simulator/test/AlwaysDeliverNetworkScheduler.java new file mode 100644 index 0000000000..59b274c335 --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/AlwaysDeliverNetworkScheduler.java @@ -0,0 +1,77 @@ +/* + * 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.simulator.test; + +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.simulator.FutureActionScheduler; +import org.apache.cassandra.simulator.RandomSource; +import org.apache.cassandra.simulator.systems.SimulatedTime; +import org.apache.cassandra.simulator.utils.LongRange; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** + * Action scheduler that simulates ideal networking conditions. Useful to rule out + * timeouts in some of the verbs to test only a specific subset of Cassandra. + */ +public class AlwaysDeliverNetworkScheduler implements FutureActionScheduler +{ + private final SimulatedTime time; + private final RandomSource randomSource; + private final LongRange schedulerDelay = new LongRange(0, 50, MICROSECONDS, NANOSECONDS); + + private final long delayNanos; + + AlwaysDeliverNetworkScheduler(SimulatedTime time, RandomSource randomSource) + { + this(time, randomSource, TimeUnit.MILLISECONDS.toNanos(10)); + } + AlwaysDeliverNetworkScheduler(SimulatedTime time, RandomSource randomSource, long dealayNanos) + { + this.time = time; + this.randomSource = randomSource; + this.delayNanos = dealayNanos; + } + public Deliver shouldDeliver(int from, int to) + { + return Deliver.DELIVER; + } + + public long messageDeadlineNanos(int from, int to) + { + return time.nanoTime() + delayNanos; + } + + public long messageTimeoutNanos(long expiresAfterNanos, long expirationIntervalNanos) + { + return expiresAfterNanos + 1; + } + + public long messageFailureNanos(int from, int to) + { + throw new IllegalStateException(); + } + + public long schedulerDelayNanos() + { + return 1; + } +} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/FixedLossNetworkScheduler.java b/test/simulator/test/org/apache/cassandra/simulator/test/FixedLossNetworkScheduler.java new file mode 100644 index 0000000000..d2f6a2eb02 --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/FixedLossNetworkScheduler.java @@ -0,0 +1,158 @@ +/* + * 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.simulator.test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.simulator.FutureActionScheduler; +import org.apache.cassandra.simulator.RandomSource; +import org.apache.cassandra.simulator.systems.SimulatedTime; +import org.apache.cassandra.simulator.utils.ChanceRange; +import org.apache.cassandra.simulator.utils.KindOfSequence; +import org.apache.cassandra.simulator.utils.LongRange; + +/** + * A scheduler that allows to configure network loss chance, that applies this chance to + * _all_ operations. Useful for testing severe loss scenarios to make sure your retry + * mechanisms work well. + */ +public class FixedLossNetworkScheduler implements FutureActionScheduler +{ + private static final Logger logger = LoggerFactory.getLogger(FixedLossNetworkScheduler.class); + final RandomSource random; + final SimulatedTime time; + + final KindOfSequence.LinkLatency normalLatency; + final KindOfSequence.LinkLatency delayLatency; + + final KindOfSequence.NetworkDecision dropMessage; + final KindOfSequence.NetworkDecision delayMessage; + + final KindOfSequence.Decision delayChance; + final LongRange delayNanos, longDelayNanos; + + FixedLossNetworkScheduler(int nodes, RandomSource random, SimulatedTime time, KindOfSequence kind, float min, float max) + { + this.random = random; + this.time = time; + this.normalLatency = kind.linkLatency(nodes, + new LongRange(2, 20, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS), + random); + this.delayLatency = kind.linkLatency(nodes, + new LongRange(20, 40, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS), + random); + + this.dropMessage = kind.networkDecision(nodes, + new ChanceRange(randomSource -> randomSource.qlog2uniformFloat(4), min, max), + random); + + this.delayMessage = kind.networkDecision(nodes, + new ChanceRange(randomSource -> randomSource.qlog2uniformFloat(4), min, max), + random); + + this.delayChance = kind.decision(new ChanceRange(randomSource -> randomSource.qlog2uniformFloat(4), min, max), + random); + + this.delayNanos = new LongRange(2, 100, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS); + this.longDelayNanos = new LongRange(2, 100, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS); + } + + private static class DeliveryPair + { + final int from; + final int to; + + private DeliveryPair(int from, int to) + { + this.from = from; + this.to = to; + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeliveryPair that = (DeliveryPair) o; + return from == that.from && to == that.to; + } + + public int hashCode() + { + return Objects.hash(from, to); + } + + public String toString() + { + return "" + from + + "->" + to; + } + } + + private Map pairs = new HashMap<>(); + private static final int TIMEOUTS_IN_A_ROW = 1; + public Deliver shouldDeliver(int from, int to) + { + DeliveryPair pair = new DeliveryPair(from, to); + if (!dropMessage.get(random, from, to)) + { + pairs.put(pair, 0); + return Deliver.DELIVER; + } + + int subsequentFailures = pairs.compute(pair, (k, v) -> v == null ? 1 : v+1); + + if (subsequentFailures > TIMEOUTS_IN_A_ROW) + { + logger.info("Delivering {} after {} failures in a row", pair, TIMEOUTS_IN_A_ROW); + pairs.put(pair, 0); + return Deliver.DELIVER; + } + + logger.info("Timing out {} for the {}th time", pair, subsequentFailures); + return Deliver.TIMEOUT; + } + + public long messageDeadlineNanos(int from, int to) + { + return time.nanoTime() + (delayMessage.get(random, from, to) + ? normalLatency.get(random, from, to) + : delayLatency.get(random, from, to)); + } + + public long messageTimeoutNanos(long expiresAfterNanos, long expirationIntervalNanos) + { + return expiresAfterNanos + random.uniform(0, expirationIntervalNanos / 2); + } + + public long messageFailureNanos(int from, int to) + { + return messageDeadlineNanos(from, to); + } + + public long schedulerDelayNanos() + { + return (delayChance.get(random) ? longDelayNanos : delayNanos).select(random); + } +} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java new file mode 100644 index 0000000000..31c8dae69d --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java @@ -0,0 +1,797 @@ +/* + * 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.simulator.test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import harry.core.Configuration; +import harry.core.Run; +import harry.ddl.ColumnSpec; +import harry.ddl.SchemaGenerators; +import harry.ddl.SchemaSpec; +import harry.generators.Surjections; +import harry.model.clock.OffsetClock; +import harry.model.sut.SystemUnderTest; +import harry.model.sut.TokenPlacementModel; +import harry.operations.Query; +import harry.runner.DefaultDataTracker; +import harry.visitors.GeneratingVisitor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.fuzz.HarryHelper; +import org.apache.cassandra.distributed.fuzz.InJvmSut; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.simulator.Action; +import org.apache.cassandra.simulator.ActionList; +import org.apache.cassandra.simulator.ActionSchedule; +import org.apache.cassandra.simulator.Actions; +import org.apache.cassandra.simulator.ClusterSimulation; +import org.apache.cassandra.simulator.Debug; +import org.apache.cassandra.simulator.FutureActionScheduler; +import org.apache.cassandra.simulator.OrderOn; +import org.apache.cassandra.simulator.RandomSource; +import org.apache.cassandra.simulator.RunnableActionScheduler; +import org.apache.cassandra.simulator.Simulation; +import org.apache.cassandra.simulator.SimulatorUtils; +import org.apache.cassandra.simulator.cluster.ClusterActionListener.NoOpListener; +import org.apache.cassandra.simulator.cluster.ClusterActions; +import org.apache.cassandra.simulator.cluster.ClusterActions.Options; +import org.apache.cassandra.simulator.harry.HarryValidatingQuery; +import org.apache.cassandra.simulator.systems.Failures; +import org.apache.cassandra.simulator.systems.InterceptedExecution; +import org.apache.cassandra.simulator.systems.InterceptingExecutor; +import org.apache.cassandra.simulator.systems.SimulatedActionTask; +import org.apache.cassandra.simulator.systems.SimulatedSystems; +import org.apache.cassandra.simulator.systems.SimulatedTime; +import org.apache.cassandra.simulator.utils.KindOfSequence; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Startup; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.sequences.SingleNodeSequences; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.utils.CloseableIterator; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.simulator.ActionSchedule.Mode.TIME_LIMITED; +import static org.apache.cassandra.simulator.ActionSchedule.Mode.UNLIMITED; +import static org.apache.cassandra.simulator.cluster.ClusterActions.Options.noActions; + +public class HarrySimulatorTest +{ + private static final Logger logger = LoggerFactory.getLogger(HarrySimulatorTest.class); + + public static void main(String... args) throws Throwable + { + new HarrySimulatorTest().harryTest(); + System.exit(0); + } + + @Test + public void harryTest() throws Exception + { + int bootstrapNode1 = 4; + int bootstrapNode2 = 8; + int bootstrapNode3 = 12; + int rowsPerPhase = 1000; + int nodesPerDc = 3; + StringBuilder rfString = new StringBuilder(); + Map rfMap = new HashMap<>(); + for (int i = 0; i < 3; i++) + { + String dc = "dc" + i; + rfMap.put(dc, 3); + if (i > 0) + rfString.append(", "); + rfString.append("'").append(dc).append("'").append(" : ").append(nodesPerDc); + } + + TokenPlacementModel.NtsReplicationFactor rf = new TokenPlacementModel.NtsReplicationFactor(rfMap); + + ConsistencyLevel cl = ALL; + + simulate(HarryHelper.defaultConfiguration() + .setSchemaProvider(new Configuration.SchemaProviderConfiguration() + { + private final Surjections.Surjection schema = schemaSpecGen("harry", "tbl"); + public SchemaSpec make(long l, SystemUnderTest systemUnderTest) + { + return schema.inflate(l); + } + }) + + .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 1)) + .setClusteringDescriptorSelector(HarryHelper.singleRowPerModification().setMaxPartitionSize(100).build()), + (simulation) -> { + simulation.cluster.stream().forEach((IInvokableInstance i) -> { + simulation.simulated.failureDetector.markUp(i.config().broadcastAddress()); + }); + + List work = new ArrayList<>(); + work.add(work(run(() -> { + for (Map.Entry> e : simulation.nodeState.nodesByDc.entrySet()) + { + List nodesInDc = e.getValue(); + for (int i = 0; i < 3; i++) + { + TokenPlacementModel.Node node = nodesInDc.get(i); + simulation.nodeState.unsafeBootstrap(node); + } + } + }))); + work.add(work(lazy(() -> simulation.clusterActions.initializeCluster(new ClusterActions.InitialConfiguration(simulation.nodeState.joined(), new int[0]))))); + work.add(work(reconfigureCMS(simulation.simulated, simulation.cluster, 2, true))); + work.add(work(simulation.clusterActions.schemaChange(1, + String.format("CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy', " + rfString + "};", + simulation.harryRun.schemaSpec.keyspace)))); + work.add(work(simulation.clusterActions.schemaChange(1, + simulation.harryRun.schemaSpec.compile().cql()))); + work.add(work(HarrySimulatorTest.generate(rowsPerPhase, simulation, cl))); + + + simulation.cluster.stream().forEach(i -> { + work.add(work(lazy(simulation.simulated, i, () -> logger.info(ClusterMetadata.current().epoch.toString())))); + }); + work.add(work(lazy(() -> validateAllLocal(simulation, simulation.nodeState.ring, rf)))); + + List registeredNodes = new ArrayList<>(Arrays.asList(bootstrapNode1, bootstrapNode2, bootstrapNode3)); + List bootstrappedNodes = new ArrayList<>(); + while (!registeredNodes.isEmpty() || !bootstrappedNodes.isEmpty()) + { + boolean shouldBootstrap = simulation.simulated.random.decide(0.5f); + if (shouldBootstrap && registeredNodes.isEmpty()) + shouldBootstrap = false; + if (!shouldBootstrap && bootstrappedNodes.isEmpty()) + shouldBootstrap = true; + + int node; + if (shouldBootstrap) + { + node = registeredNodes.remove(0); + long token = simulation.simulated.random.uniform(Long.MIN_VALUE, Long.MAX_VALUE); + work.add(work(ActionList.of(bootstrap(simulation.simulated, simulation.cluster, token, node)), + generate(rowsPerPhase, simulation, cl) + )); + simulation.cluster.stream().forEach(i -> { + work.add(work(lazy(simulation.simulated, i, () -> logger.info(ClusterMetadata.current().epoch.toString())))); + }); + work.add(work(run(() -> simulation.nodeState.bootstrap(node, token)))); + work.add(work(assertNodeState(simulation.simulated, simulation.cluster, node,NodeState.JOINED))); + bootstrappedNodes.add(node); + } + else + { + assert !bootstrappedNodes.isEmpty(); + node = bootstrappedNodes.remove(0); + work.add(work(ActionList.of(decommission(simulation.simulated, simulation.cluster, node)), + generate(rowsPerPhase, simulation, cl) + )); + simulation.cluster.stream().forEach(i -> { + work.add(work(lazy(simulation.simulated, i, () -> logger.info(ClusterMetadata.current().epoch.toString())))); + }); + work.add(work(run(() -> simulation.nodeState.decommission(node)))); + work.add(work(assertNodeState(simulation.simulated, simulation.cluster, node,NodeState.LEFT))); + } + work.add(work(lazy(() -> validateAllLocal(simulation, simulation.nodeState.ring, rf)))); + boolean tmp = shouldBootstrap; + work.add(work(run(() -> System.out.printf("Finished %s of %d and data validation!\n", tmp ? "bootstrap" : "decommission", node)))); + } + work.add(work(run(() -> System.out.println("Finished!")))); + + return arr(work.toArray(new ActionSchedule.Work[0])); + }, + (config) -> config.set("cms_default_max_retries", 10) + .set("progress_barrier_min_consistency_level", ALL) + .set("progress_barrier_default_consistency_level", ALL) + .set("progress_barrier_timeout", "600000ms") + // Backoff should be larger than read timeout, since otherwise we will simply saturate the stage with retries + .set("progress_barrier_backoff", "11000ms") + .set("cms_await_timeout", "20000ms"), + arr(), + (config) -> config + .failures(new HaltOnError()) + .threadCount(1000) + .readTimeoutNanos(SECONDS.toNanos(5)) + .writeTimeoutNanos(SECONDS.toNanos(5)) + .readTimeoutNanos(SECONDS.toNanos(10)) + .nodes(12, 12) + .dcs(3, 3) + ); + } + + /** + * Harry simulation. Assumes no scheduler delays and has ideal networking conditions. + * + * Since this is generally used to test _different_ subsystems, we plug in failures into the component + * we are testing to both reduce the noise and the surface for potential investigations. This also + * has a nice side effect of making simulations slightly faster. + */ + static abstract class HarrySimulation implements Simulation + { + protected final Configuration.ConfigurationBuilder harryConfig; + protected final ClusterActions clusterActions; + protected final SimulatedNodeState nodeState; + protected final Run harryRun; + + protected final SimulatedSystems simulated; + protected final RunnableActionScheduler scheduler; + protected final Cluster cluster; + + public HarrySimulation(SimulatedSystems simulated, RunnableActionScheduler scheduler, Cluster cluster, Configuration.ConfigurationBuilder harryConfig) + { + this.simulated = simulated; + this.scheduler = scheduler; + this.cluster = cluster; + + this.harryConfig = harryConfig; + Options options = noActions(cluster.size()); + this.clusterActions = new ClusterActions(simulated, cluster, + options, new NoOpListener(), new Debug(new EnumMap<>(Debug.Info.class), new int[0])); + + InJvmSut sut = new InJvmSut(cluster) { + public void shutdown() + { + // Let simulation shut down the cluster, as it uses `nanoTime` + } + }; + + Configuration configuration = harryConfig.setClock(() -> new OffsetClock(1000)) // todo: potentially integrate approximate clock with Simulator + .setSUT(() -> sut) + .build(); + this.harryRun = configuration.createRun(); + this.nodeState = new SimulatedNodeState(this); + } + + public void run() + { + try (CloseableIterator iter = iterator()) + { + while (iter.hasNext()) + iter.next(); + } + } + + public void close() throws Exception + { + } + } + + static class HarrySimulationBuilder extends ClusterSimulation.Builder + { + protected final Configuration.ConfigurationBuilder harryConfig; + protected final Consumer configUpdater; + + protected final Function work; + + HarrySimulationBuilder(Configuration.ConfigurationBuilder harryConfig, + Consumer configUpdater, + Function work + ) + { + this.harryConfig = harryConfig; + this.configUpdater = configUpdater; + this.work = work; + } + + public Map perVerbFutureActionSchedulers(int nodeCount, SimulatedTime time, RandomSource random) + { + return HarrySimulatorTest.networkSchedulers(nodeCount, time, random); + } + + public FutureActionScheduler futureActionScheduler(int nodeCount, SimulatedTime time, RandomSource random) + { + return new AlwaysDeliverNetworkScheduler(time, random); + } + + @Override + public ClusterSimulation create(long seed) throws IOException + { + RandomSource random = new RandomSource.Default(); + random.reset(seed); + this.harryConfig.setSeed(seed); + + return new ClusterSimulation<>(random, seed, 1, this, + configUpdater, + (simulated, scheduler, cluster, options) -> new HarrySimulation(simulated, scheduler, cluster, harryConfig) + { + + @Override + public CloseableIterator iterator() + { + // ok; and scheduler jitter is here + return new ActionSchedule(simulated.time, simulated.futureScheduler, () -> 0L, scheduler, work.apply(this)); + } + }); + } + } + + /** + * Simulation entrypoint; syntax sugar for creating a simulation. + */ + public static void simulate(Configuration.ConfigurationBuilder harryConfig, + Function work, + Consumer instanceConfigUpdater, + String[] properties, + Consumer> configure) throws IOException + { + try (WithProperties p = new WithProperties().with(properties)) + { + SimulationTestBase.simulate(new HarrySimulationBuilder(harryConfig, instanceConfigUpdater, work), + configure); + }; + } + + /** + * Custom network scheduler for testing TCM. + */ + public static Map networkSchedulers(int nodes, SimulatedTime time, RandomSource random) + { + Set extremelyLossy = new HashSet<>(Arrays.asList(Verb.TCM_ABORT_MIG, Verb.TCM_REPLICATION, + Verb.TCM_COMMIT_REQ, Verb.TCM_NOTIFY_REQ, + Verb.TCM_FETCH_CMS_LOG_REQ, Verb.TCM_FETCH_PEER_LOG_REQ, + Verb.TCM_INIT_MIG_REQ, Verb.TCM_INIT_MIG_RSP, + Verb.TCM_DISCOVER_REQ, Verb.TCM_DISCOVER_RSP)); + + Set somewhatSlow = new HashSet<>(Arrays.asList(Verb.BATCH_STORE_REQ, Verb.BATCH_STORE_RSP)); + + Set somewhatLossy = new HashSet<>(Arrays.asList(Verb.TCM_CURRENT_EPOCH_REQ, + Verb.TCM_NOTIFY_RSP, Verb.TCM_FETCH_CMS_LOG_RSP, + Verb.TCM_FETCH_PEER_LOG_RSP, Verb.TCM_COMMIT_RSP, + Verb.PAXOS2_COMMIT_REMOTE_REQ, Verb.PAXOS2_COMMIT_REMOTE_RSP, + Verb.PAXOS2_PREPARE_REQ, Verb.PAXOS2_PREPARE_RSP, + Verb.PAXOS2_PROPOSE_REQ, Verb.PAXOS2_PROPOSE_RSP, + Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, + Verb.PAXOS_PROPOSE_RSP, Verb.PAXOS_PROPOSE_REQ, + Verb.PAXOS_COMMIT_REQ, Verb.PAXOS_COMMIT_RSP)); + + Map schedulers = new HashMap<>(); + for (Verb verb : Verb.values()) + { + if (extremelyLossy.contains(verb)) + schedulers.put(verb, new FixedLossNetworkScheduler(nodes, random, time, KindOfSequence.UNIFORM, .2f, .3f)); + else if (somewhatLossy.contains(verb)) + schedulers.put(verb, new FixedLossNetworkScheduler(nodes, random, time, KindOfSequence.UNIFORM, .1f, .15f)); + else if (somewhatSlow.contains(verb)) + schedulers.put(verb, new AlwaysDeliverNetworkScheduler(time, random, TimeUnit.MILLISECONDS.toNanos(100))); + } + return schedulers; + } + + public Action reconfigureCMS(SimulatedSystems simulated, Cluster cluster, int rf, boolean inEachDc) + { + return new SimulatedActionTask("", Action.Modifiers.RELIABLE_NO_TIMEOUTS, Action.Modifiers.RELIABLE_NO_TIMEOUTS, null, simulated, + new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) cluster.get(1).executor(), + cluster.get(1).transfer((IIsolatedExecutor.SerializableRunnable) () -> { + ReplicationParams params; + if (inEachDc) + { + Map rfs = new HashMap<>(); + for (String dc : ClusterMetadata.current().directory.knownDatacenters()) + { + rfs.put(dc, rf); + } + params = ReplicationParams.ntsMeta(rfs); + } + else + { + params = ReplicationParams.simpleMeta(rf, ClusterMetadata.current().directory.knownDatacenters()); + } + ClusterMetadataService.instance().reconfigureCMS(params); + }))); + } + + /** + * Creates an action that is equivalent to starting bootstrap on the node via nodetool bootstrap, or + * simply starting a fresh node with a pre-configured token. + */ + public static Action bootstrap(SimulatedSystems simulated, Cluster cluster, long token, int node) + { + IIsolatedExecutor.SerializableRunnable runnable = () -> { + try + { + Startup.startup(() -> new PrepareJoin(ClusterMetadata.current().myNodeId(), + Collections.singleton(new Murmur3Partitioner.LongToken(token)), + ClusterMetadataService.instance().placementProvider(), + true, + true), + true, + true, + false); + } + catch (Throwable t) + { + logger.error("Could not bootstrap. Interrupting simulation.", t); + SimulatorUtils.failWithOOM(); + } + }; + + return new SimulatedActionTask("Bootstrap Node", + Action.Modifiers.NONE, + Action.Modifiers.NONE, + null, + simulated, + new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) cluster.get(node).executor(), + cluster.get(node).transfer(runnable))); + } + + public Action decommission(SimulatedSystems simulated, Cluster cluster, int node) + { + IIsolatedExecutor.SerializableRunnable runnable = () -> { + try + { + SingleNodeSequences.decommission(false, false); + } + catch (Throwable t) + { + logger.error("Could not decommission. Interrupting simulation.", t); + SimulatorUtils.failWithOOM(); + } + }; + + return new SimulatedActionTask("Decommission Node", + Action.Modifiers.NONE, + Action.Modifiers.NONE, + null, + simulated, + new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) cluster.get(node).executor(), + cluster.get(node).transfer(runnable))); + } + + public static Action run(Runnable run) + { + return new Actions.LambdaAction("", Action.Modifiers.RELIABLE_NO_TIMEOUTS, () -> { + run.run(); + return ActionList.empty(); + }); + } + + public static Action lazy(SimulatedSystems simulated, IInvokableInstance instance, IIsolatedExecutor.SerializableRunnable runnable) + { + return new SimulatedActionTask("", Action.Modifiers.RELIABLE_NO_TIMEOUTS, Action.Modifiers.RELIABLE_NO_TIMEOUTS, null, simulated, + new InterceptedExecution.InterceptedRunnableExecution((InterceptingExecutor) instance.executor(), instance.transfer(runnable))); + } + + public static Action lazy(Supplier run) + { + return new Actions.LambdaAction("", Action.Modifiers.RELIABLE_NO_TIMEOUTS, () -> ActionList.of(run.get())); + } + + private static Action assertNodeState(SimulatedSystems simulated, Cluster cluster, int i, NodeState expected) + { + return lazy(simulated, cluster.get(i), + () -> { + NodeState actual = ClusterMetadata.current().myNodeState(); + if (!actual.toString().equals(expected.toString())) + { + logger.info("Node {} state ({}) is not as expected {}", i, actual, expected); + SimulatorUtils.failWithOOM(); + } + }); + } + + /** + * Creates an action list with a fixed number of data-generating operations that conform to the given Harry configuration. + */ + public static ActionList generate(int ops, HarrySimulation simulation, ConsistencyLevel cl) + { + Action[] actions = new Action[ops]; + OrderOn orderOn = new OrderOn.Strict(actions, 2); + generate(ops, simulation, new Consumer() + { + int i = 0; + + public void accept(Action action) + { + actions[i++] = action; + } + }, + cl); + return ActionList.of(actions).orderOn(orderOn); + } + + public static void generate(int ops, HarrySimulation simulation, Consumer add, org.apache.cassandra.distributed.api.ConsistencyLevel cl) + { + SimulatedVisitExectuor visitExectuor = new SimulatedVisitExectuor(simulation, + simulation.harryRun, + cl); + GeneratingVisitor generatingVisitor = new GeneratingVisitor(simulation.harryRun, visitExectuor); + + for (int i = 0; i < ops; i++) + { + generatingVisitor.visit(simulation.harryRun.clock.nextLts()); + // A tiny chance of executing a multi-partition batch + if (ops % 10 == 0) + generatingVisitor.visit(simulation.harryRun.clock.nextLts()); + add.accept(visitExectuor.build()); + } + + } + + /** + * Create an infinite stream to generate data. + */ + public static Supplier generate(HarrySimulation simulation, org.apache.cassandra.distributed.api.ConsistencyLevel cl) + { + SimulatedVisitExectuor visitExectuor = new SimulatedVisitExectuor(simulation, + simulation.harryRun, + cl); + GeneratingVisitor generatingVisitor = new GeneratingVisitor(simulation.harryRun, visitExectuor); + + DefaultDataTracker tracker = (DefaultDataTracker) simulation.harryRun.tracker; + return new Supplier() + { + public Action get() + { + // Limit how many queries can be in-flight simultaneously to reduce noise + if (tracker.maxStarted() - tracker.maxConsecutiveFinished() == 0) + { + generatingVisitor.visit(); + return visitExectuor.build(); + } + else + { + // No-op + return run(() -> {}); + } + } + + public String toString() + { + return "Query Generator"; + } + }; + } + + /** + * Given you have used `generate` methods to generate data with Harry, you can use this method to check whether all + * data has been propagated everywhere it should be, be it via streaming, read repairs, or regular writes. + */ + public static Action validateAllLocal(HarrySimulation simulation, List owernship, TokenPlacementModel.ReplicationFactor rf) + { + return new Actions.LambdaAction("Validate", Action.Modifiers.RELIABLE_NO_TIMEOUTS, + () -> { + if (!simulation.harryRun.tracker.isFinished(simulation.harryRun.tracker.maxStarted())) + throw new IllegalStateException("Can not begin validation, as writing has not quiesced yet: " + simulation.harryRun.tracker); + List actions = new ArrayList<>(); + long maxLts = simulation.harryRun.tracker.maxStarted(); + long maxPosition = simulation.harryRun.pdSelector.maxPosition(maxLts); + logger.info("Starting validation of {} written partitions. Highest LTS is {}. Ring view: {}", maxPosition, maxLts, simulation.nodeState); + for (int position = 0; position < maxPosition; position++) + { + long minLts = simulation.harryRun.pdSelector.minLtsAt(position); + long pd = simulation.harryRun.pdSelector.pd(minLts, simulation.harryRun.schemaSpec); + Query query = Query.selectPartition(simulation.harryRun.schemaSpec, pd, false); + actions.add(new HarryValidatingQuery(simulation.simulated, simulation.cluster, rf, + simulation.harryRun, owernship, query)); + } + return ActionList.of(actions).setStrictlySequential(); + }); + } + + // todo rename + private static ActionSchedule.Work work(Action... actions) + { + return new ActionSchedule.Work(UNLIMITED, Collections.singletonList(ActionList.of(actions).setStrictlySequential())); + } + + private static ActionSchedule.Work work(ActionList... actions) + { + return new ActionSchedule.Work(UNLIMITED, Arrays.asList(actions)); + } + + private static ActionSchedule.Work interleave(long nanos, Action... actions) + { + return new ActionSchedule.Work(TIME_LIMITED, nanos, Collections.singletonList(ActionList.of(actions))); + } + + private static ActionSchedule.Work interleave(Action... actions) + { + return new ActionSchedule.Work(UNLIMITED, Collections.singletonList(ActionList.of(actions))); + } + + + /** + * Simple simulated node state. Used to closely track what is going on in the cluster and + * model placements for node-local validation. + */ + public static class SimulatedNodeState + { + public final TokenPlacementModel.Node[] nodesLookup; + public final List ring; + public final Map> nodesByDc; + public final Map idByAddr; + + public SimulatedNodeState(HarrySimulation simulation) + { + this.nodesLookup = new TokenPlacementModel.Node[simulation.cluster.size()]; + this.nodesByDc = new HashMap<>(); + this.idByAddr = new HashMap<>(); + for (int i = 0; i < simulation.cluster.size(); i++) + { + int nodeId = i + 1; + IInstanceConfig config = simulation.cluster.get(nodeId).config(); + + InetAddressAndPort addr = InetAddressAndPort.getByAddress(config.broadcastAddress()); + TokenPlacementModel.Node node = new TokenPlacementModel.Node(Long.parseLong(config.getString("initial_token")), + addr.toString(), + new TokenPlacementModel.Location(simulation.clusterActions.snitch.get().getDatacenter(addr), + simulation.clusterActions.snitch.get().getRack(addr))); + nodesLookup[i] = node; + nodesByDc.computeIfAbsent(node.location.dc, (k) -> new ArrayList<>()).add(node); + idByAddr.put(addr.toString(), config.num()); + } + this.ring = new ArrayList<>(); + } + + public int[] joined() + { + int[] joined = new int[ring.size()]; + for (int i = 0; i < ring.size(); i++) + { + joined[i] = idByAddr.get(ring.get(i).id); + } + return joined; + } + + public void unsafeBootstrap(int nodeId) + { + TokenPlacementModel.Node n = nodesLookup[nodeId - 1]; + int idx = Collections.binarySearch(ring, n); + if (idx < 0) + ring.add(-idx - 1, n); + else + ring.set(idx, n); + } + + public void unsafeBootstrap(TokenPlacementModel.Node n) + { + int idx = Collections.binarySearch(ring, n); + if (idx < 0) + ring.add(-idx - 1, n); + else + ring.set(idx, n); + } + + public void bootstrap(int nodeId, long token) + { + System.out.printf("Marking %d as bootstrapped%n", nodeId); + TokenPlacementModel.Node n = nodesLookup[nodeId - 1]; + // Update token in the lookup. Do it before search as we need ring sorted + n = withToken(n, token); + nodesLookup[nodeId - 1] = n; + + int idx = Collections.binarySearch(ring, n); + + if (idx < 0) + ring.add(-idx - 1, n); + else + ring.set(idx, n); + + // Assert sorted + TokenPlacementModel.Node prev = null; + for (TokenPlacementModel.Node node : ring) + { + if (prev != null) + assert node.token > prev.token : "Ring doesn't seem to be sorted: " + ring; + prev = node; + } + } + + public void decommission(int nodeId) + { + System.out.printf("Marking %d as decommissioned%n", nodeId); + TokenPlacementModel.Node n = nodesLookup[nodeId - 1]; + int idx = Collections.binarySearch(ring, n); + ring.remove(idx); + assertSorted(ring); + } + + private static void assertSorted(List ring) + { + // Assert sorted + TokenPlacementModel.Node prev = null; + for (TokenPlacementModel.Node node : ring) + { + if (prev != null) + assert node.token > prev.token : "Ring doesn't seem to be sorted: " + ring; + prev = node; + } + + } + private static TokenPlacementModel.Node withToken(TokenPlacementModel.Node node, long token) + { + return new TokenPlacementModel.Node(token, node.id, node.location); + } + + public String toString() + { + return "SimulatedNodeState{" + + "ring=" + ring + + '}'; + } + } + + public static T[] arr(T... arr) + { + return arr; + } + + // Use only types that can guarantee we will et 64 bits of entropy here, at least for now. + public static Surjections.Surjection schemaSpecGen(String keyspace, String prefix) + { + AtomicInteger counter = new AtomicInteger(); + return new SchemaGenerators.Builder(keyspace, () -> prefix + counter.getAndIncrement()) + .partitionKeySpec(1, 2, + ColumnSpec.int64Type, + ColumnSpec.asciiType, + ColumnSpec.textType) + .clusteringKeySpec(1, 1, + ColumnSpec.int64Type, + ColumnSpec.asciiType, + ColumnSpec.textType, + ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), + ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType), + ColumnSpec.ReversedType.getInstance(ColumnSpec.textType)) + .regularColumnSpec(5, 5, + ColumnSpec.int64Type, + ColumnSpec.asciiType(4, 128)) + .staticColumnSpec(5, 5, + ColumnSpec.int64Type, + ColumnSpec.asciiType(4, 128)) + .surjection(); + } + + public static class HaltOnError extends Failures + { + @Override + public void onFailure(Throwable t) + { + super.onFailure(t); + t.printStackTrace(); + logger.error("Caught an exception, going to halt", t); + //while (true) { } + } + } +} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java index f195f1b12d..c1f25b7d67 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java @@ -25,6 +25,74 @@ import org.junit.Test; import org.apache.cassandra.simulator.paxos.PaxosSimulationRunner; +/** + * In order to run these tests in your IDE, you need to first build a simulator jara + * + * ant simulator-jars + * + * And then run your test using the following settings (omit add-* if you are running on jdk8): + * + -Dstorage-config=$MODULE_DIR$/test/conf + -Djava.awt.headless=true + -javaagent:$MODULE_DIR$/lib/jamm-0.3.2.jar + -ea + -Dcassandra.debugrefcount=true + -Xss384k + -XX:SoftRefLRUPolicyMSPerMB=0 + -XX:ActiveProcessorCount=2 + -XX:HeapDumpPath=build/test + -Dcassandra.test.driver.connection_timeout_ms=10000 + -Dcassandra.test.driver.read_timeout_ms=24000 + -Dcassandra.memtable_row_overhead_computation_step=100 + -Dcassandra.test.use_prepared=true + -Dcassandra.test.sstableformatdevelopment=true + -Djava.security.egd=file:/dev/urandom + -Dcassandra.testtag=.jdk11 + -Dcassandra.keepBriefBrief=true + -Dcassandra.allow_simplestrategy=true + -Dcassandra.strict.runtime.checks=true + -Dcassandra.reads.thresholds.coordinator.defensive_checks_enabled=true + -Dcassandra.test.flush_local_schema_changes=false + -Dcassandra.test.messagingService.nonGracefulShutdown=true + -Dcassandra.use_nix_recursive_delete=true + -Dcie-cassandra.disable_schema_drop_log=true + -Dlog4j2.configurationFile=$MODULE_DIR$/test/conf/log4j2-simulator.xml + -Dcassandra.ring_delay_ms=10000 + -Dcassandra.tolerate_sstable_size=true + -Dcassandra.skip_sync=true + -Dcassandra.debugrefcount=false + -Dcassandra.test.simulator.determinismcheck=strict + -Dcassandra.test.simulator.print_asm=none + -javaagent:$MODULE_DIR$/build/test/lib/jars/simulator-asm.jar + -Xbootclasspath/a:$MODULE_DIR$/build/test/lib/jars/simulator-bootstrap.jar + -XX:ActiveProcessorCount=4 + -XX:-TieredCompilation + -XX:-BackgroundCompilation + -XX:CICompilerCount=1 + -XX:Tier4CompileThreshold=1000 + -XX:ReservedCodeCacheSize=256M + -Xmx16G + -Xmx4G + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports java.sql/java.sql=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED + --add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED + */ public class ShortPaxosSimulationTest { @Test diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java b/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java new file mode 100644 index 0000000000..2938bb1822 --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java @@ -0,0 +1,182 @@ +/* + * 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.simulator.test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import harry.core.Run; +import harry.model.OpSelectors; +import harry.operations.CompiledStatement; +import harry.runner.DataTracker; +import harry.visitors.MutatingRowVisitor; +import harry.visitors.VisitExecutor; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.impl.Query; +import org.apache.cassandra.simulator.Action; +import org.apache.cassandra.simulator.systems.InterceptedExecution; +import org.apache.cassandra.simulator.systems.InterceptingExecutor; +import org.apache.cassandra.simulator.systems.SimulatedActionCallable; +import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; + +/** + * Point of integration between Harry and Simulator. Creates series of stictly-ordered tasks that constitute a visit + * of a single LTS. + */ +public class SimulatedVisitExectuor extends VisitExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(SimulatedVisitExectuor.class); + + private Action action = null; + private final List lts = new ArrayList<>(); + private final List statements = new ArrayList<>(); + private final List bindings = new ArrayList<>(); + private final MutatingRowVisitor rowVisitor; + private final DataTracker tracker; + private final HarrySimulatorTest.HarrySimulation simulation; + private final ConsistencyLevel cl; + + public SimulatedVisitExectuor(HarrySimulatorTest.HarrySimulation simulation, + Run run, + ConsistencyLevel cl) + { + this.rowVisitor = new MutatingRowVisitor(run); + this.simulation = simulation; + this.tracker = run.tracker; + this.cl = cl; + } + + public Action build() + { + String query = String.join(" ", statements); + + if (statements.size() > 1) + query = String.format("BEGIN BATCH\n%s\nAPPLY BATCH;", query); + + Object[] bindingsArray = new Object[bindings.size()]; + bindings.toArray(bindingsArray); + + action = new SimulatedActionCallable("Batch", + Action.Modifiers.RELIABLE_NO_TIMEOUTS, + Action.Modifiers.RELIABLE_NO_TIMEOUTS, + simulation.simulated, + simulation.cluster.get((int) ((lts.get(0) % simulation.cluster.size()) + 1)), + new RetryingQuery(query, cl, bindingsArray)) + { + private final List localLts = new ArrayList<>(SimulatedVisitExectuor.this.lts); + + @Override + protected InterceptedExecution.InterceptedTaskExecution task() + { + return new InterceptedExecution.InterceptedTaskExecution((InterceptingExecutor) on.executor()) + { + public void run() + { + for (Long l : localLts) + tracker.beginModification(l); + + // we'll be invoked on the node's executor, but we need to ensure the task is loaded on its classloader + try { accept(on.unsafeCallOnThisThread(execute), null); } + catch (Throwable t) { accept(null, t); } + finally { execute = null; } + } + }; + } + + @Override + public void accept(Object[][] result, Throwable failure) + { + if (failure != null) + simulated.failures.accept(failure); + else + for (Long l : localLts) + tracker.endModification(l); + } + }; + + statements.clear(); + bindings.clear(); + lts.clear(); + + Action current = action; + action = null; + return current; + } + + protected void beforeLts(long lts, long pd) + { + this.lts.add(lts); + } + + protected void afterBatch(long lts, long pd, long m) + { + } + + protected void beforeBatch(long lts, long pd, long m) + { + } + + public void operation(long lts, long pd, long cd, long m, long opId, OpSelectors.OperationKind opType) + { + CompiledStatement statement = rowVisitor.perform(opType, lts, pd, cd, opId); + statements.add(statement.cql()); + Collections.addAll(bindings, statement.bindings()); + } + + protected void afterLts(long lts, long pd) + { + + } + + public void shutdown() throws InterruptedException + { + } + + private static class RetryingQuery extends Query + { + public RetryingQuery(String query, ConsistencyLevel cl, Object[] boundValues) + { + super(query, -1, cl, null, boundValues); + } + + @Override + public Object[][] call() + { + while (true) + { + try + { + return super.call(); + } + catch (UncheckedInterruptedException e) + { + throw new RuntimeException(e); + } + catch (Throwable t) + { + logger.error("Caught error while executing query. Will ignore and retry: " + t.getMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/SimulationTestBase.java b/test/simulator/test/org/apache/cassandra/simulator/test/SimulationTestBase.java index 4cceb8fa3e..93bda815b7 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/SimulationTestBase.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/SimulationTestBase.java @@ -27,6 +27,8 @@ import java.util.function.IntSupplier; import java.util.function.Predicate; import com.google.common.collect.Iterators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.ExecutorFactory; import org.apache.cassandra.distributed.Cluster; @@ -35,18 +37,8 @@ import org.apache.cassandra.distributed.api.IIsolatedExecutor; import org.apache.cassandra.distributed.impl.AbstractCluster; import org.apache.cassandra.distributed.impl.IsolatedExecutor; import org.apache.cassandra.distributed.shared.InstanceClassLoader; -import org.apache.cassandra.simulator.Action; -import org.apache.cassandra.simulator.ActionList; -import org.apache.cassandra.simulator.ActionPlan; -import org.apache.cassandra.simulator.ActionSchedule; +import org.apache.cassandra.simulator.*; import org.apache.cassandra.simulator.ActionSchedule.Work; -import org.apache.cassandra.simulator.FutureActionScheduler; -import org.apache.cassandra.simulator.RunnableActionScheduler; -import org.apache.cassandra.simulator.ClusterSimulation; -import org.apache.cassandra.simulator.Debug; -import org.apache.cassandra.simulator.RandomSource; -import org.apache.cassandra.simulator.Simulation; -import org.apache.cassandra.simulator.SimulationRunner; import org.apache.cassandra.simulator.asm.InterceptClasses; import org.apache.cassandra.simulator.asm.NemesisFieldSelectors; import org.apache.cassandra.simulator.systems.Failures; @@ -62,9 +54,10 @@ import org.apache.cassandra.simulator.utils.LongRange; import org.apache.cassandra.utils.CloseableIterator; import static java.util.concurrent.TimeUnit.MILLISECONDS; +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.simulator.ActionSchedule.Mode.STREAM_LIMITED; +import static org.apache.cassandra.simulator.ActionSchedule.Mode.TIME_LIMITED; import static org.apache.cassandra.simulator.ActionSchedule.Mode.UNLIMITED; import static org.apache.cassandra.simulator.ClusterSimulation.ISOLATE; import static org.apache.cassandra.simulator.ClusterSimulation.SHARE; @@ -76,11 +69,13 @@ import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; public class SimulationTestBase { + private static final Logger logger = LoggerFactory.getLogger(Logger.class); + static abstract class DTestClusterSimulation implements Simulation { - final SimulatedSystems simulated; - final RunnableActionScheduler scheduler; - final Cluster cluster; + protected final SimulatedSystems simulated; + protected final RunnableActionScheduler scheduler; + protected final Cluster cluster; public DTestClusterSimulation(SimulatedSystems simulated, RunnableActionScheduler scheduler, Cluster cluster) { @@ -111,15 +106,15 @@ public class SimulationTestBase } protected abstract ActionList initialize(); - + protected abstract ActionList teardown(); protected abstract ActionList execute(); public CloseableIterator iterator() { - return new ActionPlan(ActionList.of(initialize()), - Collections.singletonList(execute()), - ActionList.empty()) - .iterator(STREAM_LIMITED, -1, () -> 0L, simulated.time, scheduler, simulated.futureScheduler); + return ActionPlan.setUpTearDown(ActionList.of(initialize()), + ActionList.of(teardown())) + .encapsulate(ActionPlan.interleave(Collections.singletonList(execute()))) + .iterator(TIME_LIMITED, MINUTES.toNanos(10), () -> 0L, simulated.time, scheduler, simulated.futureScheduler); } public void run() @@ -137,36 +132,65 @@ public class SimulationTestBase } } + static class DTestClusterSimulationBuilder extends ClusterSimulation.Builder + { + protected final Function init; + protected final Function test; + protected final Function teardown; + + DTestClusterSimulationBuilder(Function init, + Function test, + Function teardown) + { + this.init = init; + this.test = test; + this.teardown = teardown; + } + + public ClusterSimulation create(long seed) throws IOException + { + RandomSource random = new RandomSource.Default(); + random.reset(seed); + + return new ClusterSimulation<>(random, seed, 1, this, + (c) -> {}, + (simulated, scheduler, cluster, options) -> new DTestClusterSimulation(simulated, scheduler, cluster) + { + protected ActionList initialize() + { + return init.apply(this); + } + + protected ActionList teardown() + { + return teardown.apply(this); + } + + protected ActionList execute() + { + return test.apply(this); + } + }); + } + } + public static void simulate(Function init, Function test, + Function teardown, Consumer> configure) throws IOException + { + simulate(new DTestClusterSimulationBuilder(init, test, teardown), + configure); + } + + public static void simulate(ClusterSimulation.Builder factory, + Consumer> configure) throws IOException { SimulationRunner.beforeAll(); long seed = System.currentTimeMillis(); - RandomSource random = new RandomSource.Default(); - random.reset(seed); - class Factory extends ClusterSimulation.Builder - { - public ClusterSimulation create(long seed) throws IOException - { - return new ClusterSimulation<>(random, seed, 1, this, - (c) -> {}, - (simulated, scheduler, cluster, options) -> new DTestClusterSimulation(simulated, scheduler, cluster) { - - protected ActionList initialize() - { - return init.apply(this); - } - - protected ActionList execute() - { - return test.apply(this); - } - }); - } - } - - Factory factory = new Factory(); + // Development seed: + //long seed = 1687184561194L; + System.out.println("Simulation seed: " + seed + "L"); configure.accept(factory); try (ClusterSimulation cluster = factory.create(seed)) { @@ -192,7 +216,7 @@ public class SimulationTestBase public static void simulate(IIsolatedExecutor.SerializableRunnable[] runnables, IIsolatedExecutor.SerializableRunnable check) { - Failures failures = new Failures(); + Failures failures = new HarrySimulatorTest.HaltOnError(); RandomSource random = new RandomSource.Default(); long seed = System.currentTimeMillis(); System.out.println("Using seed: " + seed); @@ -253,7 +277,7 @@ public class SimulationTestBase { return 0; } - }, new Debug(), failures); + }, Collections.emptyMap(), new Debug(), failures); RunnableActionScheduler runnableScheduler = new RunnableActionScheduler.RandomUniform(random); @@ -269,7 +293,6 @@ public class SimulationTestBase } }; - ActionSchedule testSchedule = new ActionSchedule(simulated.time, simulated.futureScheduler, () -> 0, runnableScheduler, new Work(UNLIMITED, Collections.singletonList(ActionList.of(entrypoint)))); Iterators.advance(testSchedule, Integer.MAX_VALUE); ActionSchedule checkSchedule = new ActionSchedule(simulated.time, simulated.futureScheduler, () -> 0, runnableScheduler, new Work(UNLIMITED, Collections.singletonList(ActionList.of(toAction(check, classLoader, factory, simulated))))); @@ -287,4 +310,9 @@ public class SimulationTestBase { return arr; } + + public static int[] arr(int... arr) + { + return arr; + } } \ No newline at end of file diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/TrivialSimulationTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/TrivialSimulationTest.java index cf8fe1b25f..548cf11671 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/TrivialSimulationTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/TrivialSimulationTest.java @@ -52,6 +52,7 @@ public class TrivialSimulationTest extends SimulationTestBase }, (simulation) -> ActionList.of(simulation.executeQuery(1, "INSERT INTO ks.tbl VALUES (1,1)", ConsistencyLevel.QUORUM), simulation.executeQuery(1, "SELECT * FROM ks.tbl WHERE pk = 1", ConsistencyLevel.QUORUM)), + (simulation) -> ActionList.of(), (config) -> config .threadCount(10) .nodes(3, 3) diff --git a/test/unit/org/apache/cassandra/SchemaLoader.java b/test/unit/org/apache/cassandra/SchemaLoader.java index f341fb6eb8..d18dd0aa96 100644 --- a/test/unit/org/apache/cassandra/SchemaLoader.java +++ b/test/unit/org/apache/cassandra/SchemaLoader.java @@ -19,20 +19,20 @@ package org.apache.cassandra; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.cassandra.auth.AuthKeyspace; +import org.junit.After; + import org.apache.cassandra.auth.AuthSchemaChangeListener; import org.apache.cassandra.auth.IAuthenticator; import org.apache.cassandra.auth.IAuthorizer; import org.apache.cassandra.auth.ICIDRAuthorizer; import org.apache.cassandra.auth.INetworkAuthorizer; import org.apache.cassandra.auth.IRoleManager; -import org.apache.cassandra.config.*; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.cql3.statements.schema.IndexTarget; @@ -48,9 +48,6 @@ import org.apache.cassandra.schema.*; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; -import org.junit.After; -import org.junit.BeforeClass; - import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_UNSAFE_JOIN; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_COMPRESSION; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_COMPRESSION_ALGO; @@ -58,14 +55,14 @@ import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; public class SchemaLoader { - @BeforeClass - public static void loadSchema() throws ConfigurationException + public static void loadSchema() { prepareServer(); + } - // Migrations aren't happy if gossiper is not started. Even if we don't use migrations though, - // some tests now expect us to start gossip for them. - startGossiper(); + public static void prepareServer() + { + ServerTestUtils.prepareServer(); } @After @@ -77,12 +74,6 @@ public class SchemaLoader Thread.sleep(10); } - public static void prepareServer() - { - ServerTestUtils.daemonInitialization(); - ServerTestUtils.prepareServer(); - } - public static void startGossiper() { // skip shadow round and endpoint collision check in tests @@ -105,24 +96,13 @@ public class SchemaLoader String ks7 = testName + "Keyspace7"; String ks_kcs = testName + "KeyCacheSpace"; String ks_rcs = testName + "RowCacheSpace"; - String ks_ccs = testName + "CounterCacheSpace"; String ks_nocommit = testName + "NoCommitlogSpace"; - String ks_prsi = testName + "PerRowSecondaryIndex"; String ks_cql = testName + "cql_keyspace"; String ks_cql_replicated = testName + "cql_keyspace_replicated"; String ks_with_transient = testName + "ks_with_transient"; AbstractType bytes = BytesType.instance; - AbstractType composite = CompositeType.getInstance(Arrays.asList(new AbstractType[]{BytesType.instance, TimeUUIDType.instance, IntegerType.instance})); - AbstractType compositeMaxMin = CompositeType.getInstance(Arrays.asList(new AbstractType[]{BytesType.instance, IntegerType.instance})); - Map> aliases = new HashMap>(); - aliases.put((byte)'b', BytesType.instance); - aliases.put((byte)'t', TimeUUIDType.instance); - aliases.put((byte)'B', ReversedType.getInstance(BytesType.instance)); - aliases.put((byte)'T', ReversedType.getInstance(TimeUUIDType.instance)); - AbstractType dynamicComposite = DynamicCompositeType.getInstance(aliases); - // Make it easy to test compaction Map compactionOptions = new HashMap(); compactionOptions.put("tombstone_compaction_interval", "1"); @@ -289,7 +269,6 @@ public class SchemaLoader DatabaseDescriptor.setAuthorizer(authorizer); DatabaseDescriptor.setNetworkAuthorizer(networkAuthorizer); DatabaseDescriptor.setCIDRAuthorizer(cidrAuthorizer); - SchemaTestUtil.announceNewKeyspace(AuthKeyspace.metadata()); DatabaseDescriptor.getRoleManager().setup(); DatabaseDescriptor.getAuthenticator().setup(); DatabaseDescriptor.getInternodeAuthenticator().setupInternode(); diff --git a/test/unit/org/apache/cassandra/ServerTestUtils.java b/test/unit/org/apache/cassandra/ServerTestUtils.java index fa59f0d67d..42a18600e6 100644 --- a/test/unit/org/apache/cassandra/ServerTestUtils.java +++ b/test/unit/org/apache/cassandra/ServerTestUtils.java @@ -20,29 +20,55 @@ package org.apache.cassandra; import java.io.IOException; import java.net.UnknownHostException; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.audit.AuditLogManager; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.big.BigTableReader; import org.apache.cassandra.io.sstable.indexsummary.IndexSummarySupport; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.AbstractEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.DistributedSchema; import org.apache.cassandra.security.ThreadAwareSecurityManager; import org.apache.cassandra.service.EmbeddedCassandraService; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Commit; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Processor; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; +import org.apache.cassandra.tcm.transformations.cms.Initialize; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; /** * Utility methodes used by SchemaLoader and CQLTester to manage the server and its state. @@ -66,7 +92,11 @@ public final class ServerTestUtils public static void daemonInitialization() { DatabaseDescriptor.daemonInitialization(); + initSnitch(); + } + public static void initSnitch() + { // Register an EndpointSnitch which returns fixed values for test. DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() { @@ -93,8 +123,32 @@ public final class ServerTestUtils }); } + public static NodeId registerLocal() + { + return registerLocal(Collections.singleton(DatabaseDescriptor.getPartitioner().getRandomToken())); + } + + public static NodeId registerLocal(Set tokens) + { + NodeId nodeId = Register.maybeRegister(); + ClusterMetadataService.instance().commit(new UnsafeJoin(nodeId, + tokens, + ClusterMetadataService.instance().placementProvider())); + SystemKeyspace.setLocalHostId(nodeId.toUUID()); + return nodeId; + } + public static void prepareServer() { + prepareServerNoRegister(); + registerLocal(); + markCMS(); + } + + public static void prepareServerNoRegister() + { + daemonInitialization(); + if (isServerPrepared) return; @@ -131,12 +185,15 @@ public final class ServerTestUtils ThreadAwareSecurityManager.install(); - Keyspace.setInitialized(); + CassandraRelevantProperties.GOSSIPER_SKIP_WAITING_TO_SETTLE.setInt(0); + initCMS(); SystemKeyspace.persistLocalMetadata(); AuditLogManager.instance.initialize(); + isServerPrepared = true; } + /** * Cleanup the directories used by the server, creating them if they do not exist. */ @@ -206,6 +263,113 @@ public final class ServerTestUtils return service; } + public static void initCMS() + { + // Effectively disable automatic snapshots using AtomicLongBackedProcessopr and LocaLLog.Sync interacts + // badly with submitting SealPeriod transformations from the log listener. In this configuration, SealPeriod + // commits performed on NonPeriodicTasks threads end up actually performing the transformations as well as + // calling the pre and post commit listeners, which is not threadsafe. In a non-test setup the processing of + // log entries is always done by the dedicated log follower thread. + DatabaseDescriptor.setMetadataSnapshotFrequency(Integer.MAX_VALUE); + + Function processorFactory = AtomicLongBackedProcessor::new; + IPartitioner partitioner = DatabaseDescriptor.getPartitioner(); + boolean addListeners = true; + ClusterMetadata initial = new ClusterMetadata(partitioner); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(initial) + .withDefaultListeners(addListeners); + LocalLog log = LocalLog.async(logSpec); + ResettableClusterMetadataService service = new ResettableClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + processorFactory.apply(log), + Commit.Replicator.NO_OP, + true); + + ClusterMetadataService.setInstance(service); + initial.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + if (!Keyspace.isInitialized()) + Keyspace.setInitialized(); + log.ready(); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + service.commit(new Initialize(ClusterMetadata.current())); + QueryProcessor.registerStatementInvalidatingListener(); + service.mark(); + } + + public static void recreateCMS() + { + assert ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.getBoolean() : "Need to set " + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION + " to true for resetCMS to work"; + // unfortunately, for now this is sometimes necessary because of the initialisation ordering with regard to + // IPartitioner. For example, if a test has a requirement to use a different partitioner to the one in yaml: + // SchemaLoader.prepareServer + // |-- SchemaLoader.prepareServerNoRegister + // | |-- ServerTestUtils.daemonInitialization(); # sets DD.partitioner according to yaml (i.e. BOP) + // | |-- ServerTestUtils.prepareServer(); # includes inititial CMS using DD partitioner + // |-- StorageService.instance.setPartitionerUnsafe(M3P) # test wants to use LongToken + // |-- ServerTestUtils.recreateCMS # recreates the CMS using the updated partitioner + ClusterMetadata initial = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + initial.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(initial) + .withStorage(LogStorage.SystemKeyspace) + .withDefaultListeners(); + LocalLog log = LocalLog.async(logSpec); + log.ready(); + ResettableClusterMetadataService cms = new ResettableClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + true); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(cms); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + cms.mark(); + } + + public static void markCMS() + { + ClusterMetadataService cms = ClusterMetadataService.instance(); + assert cms instanceof ResettableClusterMetadataService : "CMS instance is not resettable"; + ((ResettableClusterMetadataService)cms).mark(); + } + + public static void resetCMS() + { + ClusterMetadataService cms = ClusterMetadataService.instance(); + assert cms instanceof ResettableClusterMetadataService : "CMS instance is not resettable"; + ((ResettableClusterMetadataService)cms).reset(); + } + + public static class ResettableClusterMetadataService extends ClusterMetadataService + { + + private ClusterMetadata mark; + + public ResettableClusterMetadataService(PlacementProvider placementProvider, + MetadataSnapshots snapshots, + LocalLog log, + Processor processor, + Commit.Replicator replicator, + boolean isMemberOfOwnershipGroup) + { + super(placementProvider, snapshots, log, processor, replicator, isMemberOfOwnershipGroup); + mark = log.metadata(); + } + + public void mark() + { + mark = log().metadata(); + } + + public Epoch reset() + { + Epoch nextEpoch = ClusterMetadata.current().epoch.nextEpoch(); + ClusterMetadata newBaseState = mark.forceEpoch(nextEpoch); + return ClusterMetadataService.instance().commit(new ForceSnapshot(newBaseState)).epoch; + } + } + private ServerTestUtils() { } diff --git a/test/unit/org/apache/cassandra/Util.java b/test/unit/org/apache/cassandra/Util.java index b0418dda0d..7eb1b502ee 100644 --- a/test/unit/org/apache/cassandra/Util.java +++ b/test/unit/org/apache/cassandra/Util.java @@ -38,7 +38,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; @@ -54,6 +53,12 @@ import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; + +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.io.util.File; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,6 +87,12 @@ import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.CompactionTasks; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.AsciiType; import org.apache.cassandra.db.marshal.Int32Type; @@ -107,9 +118,6 @@ import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableId; import org.apache.cassandra.io.sstable.SSTableLoader; @@ -118,20 +126,18 @@ import org.apache.cassandra.io.sstable.UUIDBasedSSTableId; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; import org.apache.cassandra.io.util.DataInputPlus; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaCollection; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.pager.PagingState; import org.apache.cassandra.streaming.StreamResultFuture; import org.apache.cassandra.streaming.StreamState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.tcm.transformations.Register; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.CassandraVersion; @@ -155,8 +161,6 @@ public class Util { private static final Logger logger = LoggerFactory.getLogger(Util.class); - private static List hostIdPool = new ArrayList<>(); - public static IPartitioner testPartitioner() { return DatabaseDescriptor.getPartitioner(); @@ -272,13 +276,19 @@ public class Util /** * Creates initial set of nodes and tokens. Nodes are added to StorageService as 'normal' */ - public static void createInitialRing(StorageService ss, IPartitioner partitioner, List endpointTokens, - List keyTokens, List hosts, List hostIds, int howMany) - throws UnknownHostException + public static void createInitialRing(List endpointTokens, List keyTokens, List hosts, List hostIds, int howMany) throws UnknownHostException { - // Expand pool of host IDs as necessary + createInitialRing(endpointTokens, keyTokens, hosts, hostIds, howMany, true); + } + + public static void createInitialRing(List endpointTokens, List keyTokens, List hosts, List hostIds, int howMany, boolean bootstrap) throws UnknownHostException + { + // TODO should probably rewrite tests that use this post CEP-21 + List hostIdPool = new ArrayList<>(howMany); for (int i = hostIdPool.size(); i < howMany; i++) - hostIdPool.add(UUID.randomUUID()); + { + hostIdPool.add(ClusterMetadataTestHelper.register(i + 1).toUUID()); + } boolean endpointTokenPrefilled = endpointTokens != null && !endpointTokens.isEmpty(); for (int i=0; i endpointTokens, + List hosts, + List hostIds, + int howMany) throws UnknownHostException + { + for (int i=0; i tokens = Collections.singleton(partitioner.getRandomToken()); + + Gossiper.instance.initializeNodeUnsafe(ep, hostId, MessagingService.current_version, 1); + VersionedValue.VersionedValueFactory values = new VersionedValue.VersionedValueFactory(partitioner); + Gossiper.instance.injectApplicationState(ep, ApplicationState.TOKENS, values.tokens(tokens)); + } } public static Future compactAll(ColumnFamilyStore cfs, long gcBefore) @@ -1080,29 +1106,11 @@ public class Util assertEquals(0, ((SSTableReaderWithFilter) reader).getFilterOffHeapSize()); } - /** - * Setups Gossiper to mimic the upgrade behaviour when {@link Gossiper#isUpgradingFromVersionLowerThan(CassandraVersion)} - * or {@link Gossiper#hasMajorVersion3Nodes()} is called. - */ public static void setUpgradeFromVersion(String version) { - int v = Optional.ofNullable(Gossiper.instance.getEndpointStateForEndpoint(FBUtilities.getBroadcastAddressAndPort())) - .map(ep -> ep.getApplicationState(ApplicationState.RELEASE_VERSION)) - .map(rv -> rv.version) - .orElse(0); - - Gossiper.instance.addLocalApplicationState(ApplicationState.RELEASE_VERSION, - VersionedValue.unsafeMakeVersionedValue(version, v + 1)); - try - { - // add dummy host to avoid returning early in Gossiper.instance.upgradeFromVersionSupplier - Gossiper.instance.initializeNodeUnsafe(InetAddressAndPort.getByName("127.0.0.2"), UUID.randomUUID(), 1); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - Gossiper.instance.expireUpgradeFromVersion(); + InetAddressAndPort ep = InetAddressAndPort.getByNameUnchecked("127.0.0.10"); + Register.register(new NodeAddresses(ep), + new NodeVersion(new CassandraVersion(version), Version.OLD)); } /** diff --git a/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java b/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java index b5c0e876ee..fb7a57a352 100644 --- a/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java +++ b/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java @@ -60,14 +60,11 @@ public class AllowAllCIDRAuthorizerTest extends CQLTester @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); - SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(), new AuthTestUtils.LocalPasswordAuthenticator(), new AuthTestUtils.LocalCassandraAuthorizer(), new AuthTestUtils.LocalCassandraNetworkAuthorizer(), new AuthTestUtils.LocalAllowAllCIDRAuthorizer()); - AuthCacheService.initializeAndRegisterCaches(); setupSuperUser(); } diff --git a/test/unit/org/apache/cassandra/auth/CassandraAuthorizerTest.java b/test/unit/org/apache/cassandra/auth/CassandraAuthorizerTest.java index 643b2dffbf..7ead4e4118 100644 --- a/test/unit/org/apache/cassandra/auth/CassandraAuthorizerTest.java +++ b/test/unit/org/apache/cassandra/auth/CassandraAuthorizerTest.java @@ -34,10 +34,9 @@ public class CassandraAuthorizerTest extends CQLTester private static final String PASSWORD = "secret"; @BeforeClass - public static void setupClass() + public static void setupAuth() { CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java index ce0ea16519..89fc781143 100644 --- a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java +++ b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java @@ -70,8 +70,6 @@ public class CassandraCIDRAuthorizerEnforceModeTest extends CQLTester @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); - SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(), new AuthTestUtils.LocalPasswordAuthenticator(), new AuthTestUtils.LocalCassandraAuthorizer(), diff --git a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java index fe92b2c886..54fe18bc2d 100644 --- a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java +++ b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java @@ -59,8 +59,6 @@ public class CassandraCIDRAuthorizerMonitorModeTest extends CQLTester @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); - SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(), new AuthTestUtils.LocalPasswordAuthenticator(), new AuthTestUtils.LocalCassandraAuthorizer(), diff --git a/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java b/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java index ecfa790a2e..b255ad59ff 100644 --- a/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java +++ b/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java @@ -59,7 +59,6 @@ public class CassandraNetworkAuthorizerTest extends CQLTester @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(), new AuthTestUtils.LocalPasswordAuthenticator(), new AuthTestUtils.LocalCassandraAuthorizer(), diff --git a/test/unit/org/apache/cassandra/auth/CassandraRoleManagerTest.java b/test/unit/org/apache/cassandra/auth/CassandraRoleManagerTest.java index 7b6b910a86..658d4b2bb8 100644 --- a/test/unit/org/apache/cassandra/auth/CassandraRoleManagerTest.java +++ b/test/unit/org/apache/cassandra/auth/CassandraRoleManagerTest.java @@ -21,7 +21,6 @@ package org.apache.cassandra.auth; import java.util.Map; import java.util.Set; -import com.google.common.collect.Iterables; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -29,9 +28,7 @@ import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.auth.AuthTestUtils.*; @@ -44,15 +41,11 @@ public class CassandraRoleManagerTest public static void setupClass() { SchemaLoader.prepareServer(); - // create the system_auth keyspace so the IRoleManager can function as normal - SchemaLoader.createKeyspace(SchemaConstants.AUTH_KEYSPACE_NAME, - KeyspaceParams.simple(1), - Iterables.toArray(AuthKeyspace.metadata().tables, TableMetadata.class)); // We start StorageService because confirmFastRoleSetup confirms that CassandraRoleManager will // take a faster path once the cluster is already setup, which includes checking MessagingService // and issuing queries with QueryProcessor.process, which uses TokenMetadata DatabaseDescriptor.daemonInitialization(); - StorageService.instance.initServer(0); + StorageService.instance.initServer(); AuthCacheService.initializeAndRegisterCaches(); } diff --git a/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java index 6bb83b3b63..1d71025cbb 100644 --- a/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java +++ b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java @@ -21,7 +21,6 @@ package org.apache.cassandra.auth; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; @@ -31,15 +30,12 @@ import static org.mindrot.jbcrypt.BCrypt.hashpw; public class CreateAndAlterRoleTest extends CQLTester { @BeforeClass - public static void setUpClass() + public static void setUpAuth() { - ServerTestUtils.daemonInitialization(); - DatabaseDescriptor.setPermissionsValidity(0); DatabaseDescriptor.setRolesValidity(0); DatabaseDescriptor.setCredentialsValidity(0); - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/auth/GrantAndRevokeTest.java b/test/unit/org/apache/cassandra/auth/GrantAndRevokeTest.java index eeff052ed0..55e1135cd4 100644 --- a/test/unit/org/apache/cassandra/auth/GrantAndRevokeTest.java +++ b/test/unit/org/apache/cassandra/auth/GrantAndRevokeTest.java @@ -35,11 +35,10 @@ public class GrantAndRevokeTest extends CQLTester private static final String pass = "12345"; @BeforeClass - public static void setUpClass() + public static void setUpAuth() { ServerTestUtils.daemonInitialization(); DatabaseDescriptor.setPermissionsValidity(0); - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java index 0dc7984f26..48d8a82c23 100644 --- a/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java @@ -71,9 +71,8 @@ public class MutualTlsAuthenticatorTest @BeforeClass public static void setup() { - SchemaLoader.loadSchema(); - DatabaseDescriptor.daemonInitialization(); - StorageService.instance.initServer(0); + SchemaLoader.prepareServer(); + StorageService.instance.initServer(); ((CassandraRoleManager)DatabaseDescriptor.getRoleManager()).loadIdentityStatement(); final Config config = DatabaseDescriptor.getRawConfig(); config.client_encryption_options = config.client_encryption_options.withEnabled(true) diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java index 37f8d062f9..d9fc3d0a3c 100644 --- a/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/MutualTlsInternodeAuthenticatorTest.java @@ -41,7 +41,6 @@ import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain; import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND; @@ -74,9 +73,7 @@ public class MutualTlsInternodeAuthenticatorTest public static void initialize() { CASSANDRA_CONFIG.setString("cassandra-mtls.yaml"); - SchemaLoader.loadSchema(); - DatabaseDescriptor.daemonInitialization(); - StorageService.instance.initServer(0); + SchemaLoader.prepareServer(); } @Before diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java index f4269a72fa..6a522ca853 100644 --- a/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java @@ -33,7 +33,6 @@ import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.service.StorageService; import static org.apache.cassandra.auth.AuthTestUtils.getMockInetAddress; import static org.junit.Assert.assertNotNull; @@ -47,9 +46,8 @@ public class MutualTlsWithPasswordFallbackAuthenticatorTest @BeforeClass public static void initialize() { - SchemaLoader.loadSchema(); DatabaseDescriptor.daemonInitialization(); - StorageService.instance.initServer(0); + SchemaLoader.loadSchema(); Config config = DatabaseDescriptor.getRawConfig(); config.client_encryption_options = config.client_encryption_options.withEnabled(true) .withRequireClientAuth(true); diff --git a/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java index 0a8efc25cc..91bee98794 100644 --- a/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/PasswordAuthenticatorTest.java @@ -27,12 +27,12 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.ServerTestUtils; import org.mindrot.jbcrypt.BCrypt; import com.datastax.driver.core.Authenticator; import com.datastax.driver.core.EndPoint; import com.datastax.driver.core.PlainTextAuthProvider; -import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; @@ -60,9 +60,9 @@ public class PasswordAuthenticatorTest extends CQLTester @BeforeClass public static void setupClass() throws Exception { - SchemaLoader.loadSchema(); + ServerTestUtils.prepareServerNoRegister(); DatabaseDescriptor.daemonInitialization(); - StorageService.instance.initServer(0); + StorageService.instance.initServer(); } @Before diff --git a/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java b/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java index 8a224ebdcd..49130f7e8d 100644 --- a/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java +++ b/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java @@ -230,7 +230,7 @@ public class RoleOptionsTest } - public void setup() + public void setup(boolean asyncRoleSetup) { } diff --git a/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java b/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java index 90a1d73c2a..d267a4b96a 100644 --- a/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java +++ b/test/unit/org/apache/cassandra/batchlog/BatchlogManagerTest.java @@ -17,21 +17,22 @@ */ package org.apache.cassandra.batchlog; -import java.util.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.concurrent.ExecutionException; import com.google.common.collect.Lists; - -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.Util.PartitionerSwitcher; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DecoratedKey; @@ -46,9 +47,11 @@ import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; @@ -58,7 +61,10 @@ import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.TimeUUID.Generator.atUnixMillis; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class BatchlogManagerTest { @@ -95,10 +101,7 @@ public class BatchlogManagerTest @Before public void setUp() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); InetAddressAndPort localhost = InetAddressAndPort.getByName("127.0.0.1"); - metadata.updateNormalToken(Util.token("A"), localhost); - metadata.updateHostId(UUID.randomUUID(), localhost); Keyspace.open(SchemaConstants.SYSTEM_KEYSPACE_NAME).getColumnFamilyStore(SystemKeyspace.BATCHES).truncateBlocking(); } @@ -345,7 +348,7 @@ public class BatchlogManagerTest @Test public void testReplayWithNoPeers() throws Exception { - StorageService.instance.getTokenMetadata().removeEndpoint(InetAddressAndPort.getByName("127.0.0.1")); +// StorageService.instance.getTokenMetadata().removeEndpoint(InetAddressAndPort.getByName("127.0.0.1")); long initialAllBatches = BatchlogManager.instance.countAllBatches(); long initialReplayedBatches = BatchlogManager.instance.getTotalBatchesReplayed(); diff --git a/test/unit/org/apache/cassandra/cql3/AlterSchemaStatementTest.java b/test/unit/org/apache/cassandra/cql3/AlterSchemaStatementTest.java new file mode 100644 index 0000000000..4c0f833504 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/AlterSchemaStatementTest.java @@ -0,0 +1,67 @@ +/* + * 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.cql3; + +import java.net.InetSocketAddress; + +import org.junit.Test; + +import org.apache.cassandra.schema.SchemaTransformation; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.MD5Digest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AlterSchemaStatementTest extends CQLTester +{ + // For the purposes of these tests, the specific DDL statements aren't especially relevant. + // When acting as a SchemaTransformation, all AlterSchemaStatements must be able to supply a + // CQL string for serialization in the cluster metadata log. + private static final String[] stmts = new String[] + { + "CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", + "CREATE TABLE ks.t1 (k int PRIMARY KEY)", + "ALTER MATERIALIZED VIEW ks.v1 WITH compaction = { 'class' : 'LeveledCompactionStrategy' }", + "ALTER TABLE ks.t1 ADD v int" + }; + private final ClientState clientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 1234)); + + @Test + public void testParsingSetsCQLString() throws Throwable + { + for (String cql : stmts) + { + CQLStatement stmt = QueryProcessor.getStatement(cql, clientState); + assertTrue(stmt instanceof SchemaTransformation); + assertEquals(cql, ((SchemaTransformation) stmt).cql()); + } + } + + @Test + public void testPreparingSetsCQLString() throws Throwable + { + for (String cql : stmts) + { + MD5Digest stmtId = QueryProcessor.instance.prepare(cql, clientState).statementId; + QueryHandler.Prepared prepared = QueryProcessor.instance.getPrepared(stmtId); + assertEquals(prepared.rawCQLStatement, ((SchemaTransformation)prepared.statement).cql()); + } + } +} diff --git a/test/unit/org/apache/cassandra/cql3/BatchTest.java b/test/unit/org/apache/cassandra/cql3/BatchTest.java index 330271e982..6e2a65ecdd 100644 --- a/test/unit/org/apache/cassandra/cql3/BatchTest.java +++ b/test/unit/org/apache/cassandra/cql3/BatchTest.java @@ -33,7 +33,7 @@ import org.junit.Test; import java.io.IOException; -public class BatchTest extends CQLTester +public class BatchTest { private static EmbeddedCassandraService cassandra; diff --git a/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java b/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java index 55865b1573..44ada8e917 100644 --- a/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java +++ b/test/unit/org/apache/cassandra/cql3/CDCStatementTest.java @@ -28,11 +28,10 @@ import org.apache.cassandra.config.DatabaseDescriptor; public class CDCStatementTest extends CQLTester { @BeforeClass - public static void setUpClass() + public static void enableCDC() { ServerTestUtils.daemonInitialization(); DatabaseDescriptor.setCDCEnabled(true); - CQLTester.setUpClass(); } @Test diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index 63f297dd51..80a87925ae 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -43,7 +43,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -93,11 +92,9 @@ import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.auth.AuthCacheService; -import org.apache.cassandra.auth.AuthKeyspace; import org.apache.cassandra.auth.AuthSchemaChangeListener; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.auth.IRoleManager; -import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DataStorageSpec; @@ -146,7 +143,6 @@ import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileSystems; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.schema.IndexMetadata; @@ -154,7 +150,6 @@ import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.SchemaKeyspace; -import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.service.ClientState; @@ -162,6 +157,7 @@ import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.transport.Event; import org.apache.cassandra.transport.Message; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.transport.Server; import org.apache.cassandra.transport.SimpleClient; @@ -180,8 +176,8 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_DRIVE import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_REUSE_PREPARED; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_ROW_CACHE_SIZE; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_USE_PREPARED; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -260,6 +256,7 @@ public abstract class CQLTester checkProtocolVersion(); nativeAddr = InetAddress.getLoopbackAddress(); + nativePort = getAutomaticallyAllocatedPort(nativeAddr); } private List keyspaces = new ArrayList<>(); @@ -382,16 +379,25 @@ public abstract class CQLTester return new JMXServiceURL(String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", jmxHost, jmxPort)); } + /** + * If a fixture needs to use a specific partitioner other than M3P, it should shadow this method + * with an implementation like: + * @BeforeClass + * public static void setUpClass() // overrides CQLTester.setUpClass() + * { + * daemonInitialization(); + * DatabaseDescriptor.setPartitionerUnsafe(ByteOrderedPartitioner.instance); + * prepareServer(); + * } + */ @BeforeClass public static void setUpClass() { CassandraRelevantProperties.SUPERUSER_SETUP_DELAY_MS.setLong(0); ServerTestUtils.daemonInitialization(); - if (ROW_CACHE_SIZE_IN_MIB > 0) DatabaseDescriptor.setRowCacheSizeInMiB(ROW_CACHE_SIZE_IN_MIB); StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); - // Once per-JVM is enough prepareServer(); } @@ -412,14 +418,11 @@ public abstract class CQLTester if (reusePrepared) QueryProcessor.clearInternalStatementsCache(); - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - if (jmxServer != null && jmxServer instanceof RMIConnectorServer) { try { - ((RMIConnectorServer) jmxServer).stop(); + jmxServer.stop(); } catch (IOException e) { @@ -438,18 +441,11 @@ public abstract class CQLTester @After public void afterTest() throws Throwable { - dropPerTestKeyspace(); - + ServerTestUtils.resetCMS(); // Restore standard behavior in case it was changed usePrepared = USE_PREPARED_VALUES; reusePrepared = REUSE_PREPARED; - final List keyspacesToDrop = copy(keyspaces); - final List tablesToDrop = copy(tables); - final List viewsToDrop = copy(views); - final List typesToDrop = copy(types); - final List functionsToDrop = copy(functions); - final List aggregatesToDrop = copy(aggregates); keyspaces = null; tables = null; views = null; @@ -457,62 +453,14 @@ public abstract class CQLTester functions = null; aggregates = null; user = null; - - // We want to clean up after the test, but dropping a table is rather long so just do that asynchronously - ScheduledExecutors.optionalTasks.execute(new Runnable() - { - public void run() - { - try - { - for (int i = viewsToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP MATERIALIZED VIEW IF EXISTS %s.%s", KEYSPACE, viewsToDrop.get(i))); - - for (int i = tablesToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP TABLE IF EXISTS %s.%s", KEYSPACE, tablesToDrop.get(i))); - - for (int i = aggregatesToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP AGGREGATE IF EXISTS %s", aggregatesToDrop.get(i))); - - for (int i = functionsToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP FUNCTION IF EXISTS %s", functionsToDrop.get(i))); - - for (int i = typesToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP TYPE IF EXISTS %s.%s", KEYSPACE, typesToDrop.get(i))); - - for (int i = keyspacesToDrop.size() - 1; i >= 0; i--) - schemaChange(String.format("DROP KEYSPACE IF EXISTS %s", keyspacesToDrop.get(i))); - - // Dropping doesn't delete the sstables. It's not a huge deal but it's cleaner to cleanup after us - // Thas said, we shouldn't delete blindly before the TransactionLogs.SSTableTidier for the table we drop - // have run or they will be unhappy. Since those taks are scheduled on StorageService.tasks and that's - // mono-threaded, just push a task on the queue to find when it's empty. No perfect but good enough. - - final CountDownLatch latch = new CountDownLatch(1); - ScheduledExecutors.nonPeriodicTasks.execute(new Runnable() - { - public void run() - { - latch.countDown(); - } - }); - latch.await(2, TimeUnit.SECONDS); - - removeAllSSTables(KEYSPACE, tablesToDrop); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - }); } protected void resetSchema() throws Throwable { for (TableMetadata table : SchemaKeyspace.metadata().tables) execute(String.format("TRUNCATE %s", table)); - Schema.instance.loadFromDisk(); + // TODO Fix this by resetting schema via CMS +// Schema.instance.loadFromDisk(); beforeTest(); } @@ -573,7 +521,8 @@ public abstract class CQLTester }; DatabaseDescriptor.setRoleManager(roleManager); - SchemaTestUtil.addOrUpdateKeyspace(AuthKeyspace.metadata(), true); + //TODO + //MigrationManager.announceNewKeyspace(AuthKeyspace.metadata(), true); DatabaseDescriptor.getRoleManager().setup(); DatabaseDescriptor.getAuthenticator().setup(); DatabaseDescriptor.getAuthorizer().setup(); @@ -587,7 +536,7 @@ public abstract class CQLTester /** * Initialize Native Transport for test that need it. */ - protected static void requireNetwork() throws ConfigurationException + public static void requireNetwork() throws ConfigurationException { requireNetwork(server -> {}, cluster -> {}); } @@ -1459,7 +1408,9 @@ public abstract class CQLTester QueryOptions options = QueryOptions.forInternalCalls(Collections.emptyList()); - return statement.executeLocally(queryState, options); + ResultMessage result = statement.executeLocally(queryState, options); + ClusterMetadataService.instance().log().waitForHighestConsecutive(); + return result; } catch (Exception e) { diff --git a/test/unit/org/apache/cassandra/cql3/CustomNowInSecondsTest.java b/test/unit/org/apache/cassandra/cql3/CustomNowInSecondsTest.java index c24540970a..699bbddfa0 100644 --- a/test/unit/org/apache/cassandra/cql3/CustomNowInSecondsTest.java +++ b/test/unit/org/apache/cassandra/cql3/CustomNowInSecondsTest.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.cql3.statements.BatchStatement; import org.apache.cassandra.cql3.statements.ModificationStatement; import org.apache.cassandra.db.ConsistencyLevel; @@ -48,11 +47,8 @@ import static org.junit.Assert.assertEquals; public class CustomNowInSecondsTest extends CQLTester { @BeforeClass - public static void setUpClass() + public static void setupNetwork() { - ServerTestUtils.daemonInitialization(); - - prepareServer(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java b/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java index 3f5f75abd8..5908e13068 100644 --- a/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java +++ b/test/unit/org/apache/cassandra/cql3/KeyCacheCqlTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.UUID; import java.util.concurrent.Callable; import org.junit.Assert; @@ -39,10 +40,11 @@ import org.apache.cassandra.metrics.CacheMetrics; import org.apache.cassandra.metrics.CassandraMetricsRegistry; import org.apache.cassandra.schema.CachingParams; import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaKeyspace; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -314,8 +316,12 @@ public class KeyCacheCqlTest extends CQLTester assertEquals(500, result.size()); } + Epoch preDropEpoch = ClusterMetadata.current().schema.lastModified(); dropTable("DROP TABLE %s"); - assert Schema.instance.isSameVersion(SchemaKeyspace.calculateSchemaDigest()); + Epoch postDropEpoch = ClusterMetadata.current().schema.lastModified(); + assertEquals(preDropEpoch.nextEpoch(), postDropEpoch); + // for now, we keep version as a UUID, but it's simply constructed from the schema epoch + assertEquals(new UUID(0L, postDropEpoch.getEpoch()), Schema.instance.getVersion()); //Test loading for a dropped 2i/table CacheService.instance.keyCache.clear(); diff --git a/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java b/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java index 49195f57cf..30e12ea173 100644 --- a/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java +++ b/test/unit/org/apache/cassandra/cql3/OutOfSpaceTest.java @@ -21,8 +21,10 @@ import java.io.Closeable; import java.util.concurrent.ExecutionException; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.Config.DiskFailurePolicy; import org.apache.cassandra.config.DatabaseDescriptor; @@ -30,6 +32,7 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.schema.TableId; @@ -43,6 +46,21 @@ import static org.junit.Assert.fail; */ public class OutOfSpaceTest extends CQLTester { + + /** + * Shadows the same method on the superclass as we don't want to join the ring + * because this test depends on local ranges being null and has done since its + * introduction. + * TODO investigate whether this is correct. + */ + @BeforeClass + public static void setUpClass() + { DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + ServerTestUtils.markCMS(); + } + @Test public void testFlushUnwriteableDie() throws Throwable { @@ -116,10 +134,9 @@ public class OutOfSpaceTest extends CQLTester { try { - Keyspace.open(KEYSPACE) - .getColumnFamilyStore(currentTable()) - .forceFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS) - .get(); + ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(currentTable()); + cfs.getDiskBoundaries().invalidate(); + cfs.forceFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS).get(); fail("FSWriteError expected."); } catch (ExecutionException e) diff --git a/test/unit/org/apache/cassandra/cql3/PagingTest.java b/test/unit/org/apache/cassandra/cql3/PagingTest.java index 75d73e5d0d..919ef8722b 100644 --- a/test/unit/org/apache/cassandra/cql3/PagingTest.java +++ b/test/unit/org/apache/cassandra/cql3/PagingTest.java @@ -31,16 +31,16 @@ import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; - -import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; -import org.apache.cassandra.locator.*; +import org.apache.cassandra.locator.AbstractEndpointSnitch; +import org.apache.cassandra.locator.IEndpointSnitch; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.service.EmbeddedCassandraService; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; public class PagingTest @@ -128,8 +128,8 @@ public class PagingTest } }; DatabaseDescriptor.setEndpointSnitch(snitch); - StorageService.instance.getTokenMetadata().clearUnsafe(); - StorageService.instance.getTokenMetadata().updateNormalToken(new LongToken(5097162189738624638L), FBUtilities.getBroadcastAddressAndPort()); +// StorageService.instance.getTokenMetadata().clearUnsafe(); +// StorageService.instance.getTokenMetadata().updateNormalToken(new LongToken(5097162189738624638L), FBUtilities.getBroadcastAddressAndPort()); session.execute(createTableStatement); for (int i = 0; i < 110; i++) diff --git a/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java b/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java index 2b2b78a734..aaffb7277e 100644 --- a/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java +++ b/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java @@ -18,6 +18,7 @@ package org.apache.cassandra.cql3; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; @@ -32,6 +33,7 @@ import java.util.TreeMap; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; +import org.junit.Assert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,8 +42,12 @@ import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeVersion; import org.apache.cassandra.utils.AbstractTypeGenerators; import org.apache.cassandra.utils.AbstractTypeGenerators.TypeGenBuilder; import org.apache.cassandra.utils.CassandraGenerators; @@ -54,6 +60,8 @@ import org.quicktheories.generators.SourceDSL; import org.quicktheories.impl.JavaRandom; import static org.apache.cassandra.utils.Generators.IDENTIFIER_GEN; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class RandomSchemaTest extends CQLTester.InMemory { @@ -113,6 +121,7 @@ public class RandomSchemaTest extends CQLTester.InMemory // just to make the CREATE TABLE stmt easier to read for CUSTOM types createTable = createTable.replaceAll("org.apache.cassandra.db.marshal.", ""); createTable(KEYSPACE, createTable); + serde(ClusterMetadata.current(), metadata); Gen dataGen = CassandraGenerators.data(metadata, domainGen); String insertStmt = insertStmt(metadata); @@ -160,6 +169,26 @@ public class RandomSchemaTest extends CQLTester.InMemory }); } + private void serde(ClusterMetadata metadata, TableMetadata tableMetadata) throws IOException + { + assertTrue(metadata.schema.getKeyspaces().containsKeyspace(tableMetadata.keyspace)); + assertNotNull(metadata.schema.getKeyspace(tableMetadata.keyspace).getColumnFamilyStore(tableMetadata.name)); + + DataOutputBuffer dop = new DataOutputBuffer((int) ClusterMetadata.serializer.serializedSize(metadata, NodeVersion.CURRENT_METADATA_VERSION)); + ClusterMetadata.serializer.serialize(metadata, dop, NodeVersion.CURRENT_METADATA_VERSION); + + try (DataInputBuffer dip = new DataInputBuffer(dop.buffer(), true)) + { + ClusterMetadata deserCm = ClusterMetadata.serializer.deserialize(dip, NodeVersion.CURRENT_METADATA_VERSION); + if (!metadata.equals(deserCm)) + { + metadata.dumpDiff(deserCm); + Assert.fail("Metadata mismatch"); + } + + } + } + private void maybeCreateUDTs(TableMetadata metadata) { Set udts = CassandraGenerators.extractUDTs(metadata); diff --git a/test/unit/org/apache/cassandra/cql3/ViewTest.java b/test/unit/org/apache/cassandra/cql3/ViewTest.java index fa98a2f3be..ac0ef6b142 100644 --- a/test/unit/org/apache/cassandra/cql3/ViewTest.java +++ b/test/unit/org/apache/cassandra/cql3/ViewTest.java @@ -424,7 +424,7 @@ public class ViewTest extends ViewAbstractTest assertRowsNet(executeViewNet("SELECT * FROM %s"), row(1, 0)); } - private void testViewBuilderResume(int concurrentViewBuilders) throws Throwable + private void testViewBuilderResumeHelper(int concurrentViewBuilders) throws Throwable { createTable("CREATE TABLE %s (" + "k int, " + @@ -481,7 +481,7 @@ public class ViewTest extends ViewAbstractTest { for (int i = 1; i <= 8; i *= 2) { - testViewBuilderResume(i); + testViewBuilderResumeHelper(i); } } diff --git a/test/unit/org/apache/cassandra/cql3/functions/masking/ColumnMaskTester.java b/test/unit/org/apache/cassandra/cql3/functions/masking/ColumnMaskTester.java index 93f8b001f6..f5a0b702b9 100644 --- a/test/unit/org/apache/cassandra/cql3/functions/masking/ColumnMaskTester.java +++ b/test/unit/org/apache/cassandra/cql3/functions/masking/ColumnMaskTester.java @@ -60,7 +60,6 @@ public class ColumnMaskTester extends CQLTester @BeforeClass public static void beforeClass() { - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/cql3/functions/masking/SelectMaskedPermissionTest.java b/test/unit/org/apache/cassandra/cql3/functions/masking/SelectMaskedPermissionTest.java index 81606ff6b5..34e491f2f6 100644 --- a/test/unit/org/apache/cassandra/cql3/functions/masking/SelectMaskedPermissionTest.java +++ b/test/unit/org/apache/cassandra/cql3/functions/masking/SelectMaskedPermissionTest.java @@ -45,7 +45,6 @@ public class SelectMaskedPermissionTest extends CQLTester DatabaseDescriptor.setDynamicDataMaskingEnabled(true); DatabaseDescriptor.setPermissionsValidity(0); DatabaseDescriptor.setRolesValidity(0); - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/cql3/functions/masking/UnmaskPermissionTest.java b/test/unit/org/apache/cassandra/cql3/functions/masking/UnmaskPermissionTest.java index 5a1cdc2d2e..2c862250c1 100644 --- a/test/unit/org/apache/cassandra/cql3/functions/masking/UnmaskPermissionTest.java +++ b/test/unit/org/apache/cassandra/cql3/functions/masking/UnmaskPermissionTest.java @@ -65,7 +65,6 @@ public class UnmaskPermissionTest extends CQLTester DatabaseDescriptor.setDynamicDataMaskingEnabled(true); DatabaseDescriptor.setPermissionsValidity(0); DatabaseDescriptor.setRolesValidity(0); - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); } diff --git a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java index 2339f5d8fe..9f20aea4c0 100644 --- a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java +++ b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java @@ -26,7 +26,6 @@ import java.util.List; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.schema.Schema; @@ -41,6 +40,7 @@ import org.apache.cassandra.service.QueryState; import org.apache.cassandra.utils.ByteBufferUtil; import static java.util.Arrays.asList; +import static org.apache.cassandra.ServerTestUtils.daemonInitialization; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -55,10 +55,8 @@ public class SelectionColumnMappingTest extends CQLTester @BeforeClass public static void setUpClass() // overrides CQLTester.setUpClass() { - ServerTestUtils.daemonInitialization(); - + daemonInitialization(); DatabaseDescriptor.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - prepareServer(); } diff --git a/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java index fc9e8fb135..1be22340d8 100644 --- a/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java +++ b/test/unit/org/apache/cassandra/cql3/statements/DescribeStatementTest.java @@ -35,17 +35,17 @@ import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.ProtocolVersion; import static java.lang.String.format; import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME; import static org.apache.cassandra.schema.SchemaConstants.DISTRIBUTED_KEYSPACE_NAME; +import static org.apache.cassandra.schema.SchemaConstants.METADATA_KEYSPACE_NAME; import static org.apache.cassandra.schema.SchemaConstants.SCHEMA_KEYSPACE_NAME; import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; import static org.apache.cassandra.schema.SchemaConstants.TRACE_KEYSPACE_NAME; @@ -311,6 +311,7 @@ public class DescribeStatementTest extends CQLTester row(KEYSPACE_PER_TEST, "keyspace", KEYSPACE_PER_TEST), row(SYSTEM_KEYSPACE_NAME, "keyspace", SYSTEM_KEYSPACE_NAME), row(AUTH_KEYSPACE_NAME, "keyspace", AUTH_KEYSPACE_NAME), + row(METADATA_KEYSPACE_NAME, "keyspace", METADATA_KEYSPACE_NAME), row(DISTRIBUTED_KEYSPACE_NAME, "keyspace", DISTRIBUTED_KEYSPACE_NAME), row(SCHEMA_KEYSPACE_NAME, "keyspace", SCHEMA_KEYSPACE_NAME), row(TRACE_KEYSPACE_NAME, "keyspace", TRACE_KEYSPACE_NAME), @@ -455,10 +456,9 @@ public class DescribeStatementTest extends CQLTester "ByteOrderedPartitioner", DatabaseDescriptor.getEndpointSnitch().getClass().getName())); } - - TokenMetadata tokenMetadata = StorageService.instance.getTokenMetadata(); - Token token = tokenMetadata.sortedTokens().get(0); - InetAddressAndPort addressAndPort = tokenMetadata.getAllEndpoints().iterator().next(); + ClusterMetadata metadata = ClusterMetadata.current(); + Token token = metadata.tokenMap.tokens().get(0); + InetAddressAndPort addressAndPort = metadata.directory.allAddresses().iterator().next(); assertRowsNet(executeDescribeNet(KEYSPACE_PER_TEST, "DESCRIBE CLUSTER"), row("Test Cluster", diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java index c4cdca1e57..1ed84b4d53 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/FrozenCollectionsTest.java @@ -26,7 +26,6 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.db.marshal.*; @@ -37,6 +36,7 @@ import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.ServerTestUtils.daemonInitialization; import static org.junit.Assert.assertEquals; public class FrozenCollectionsTest extends CQLTester @@ -44,10 +44,9 @@ public class FrozenCollectionsTest extends CQLTester @BeforeClass public static void setUpClass() // overrides CQLTester.setUpClass() { - ServerTestUtils.daemonInitialization(); + daemonInitialization(); // Selecting partitioner for a table is not exposed on CREATE TABLE. StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - prepareServer(); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java index f97b4fa766..00b8ae681f 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java @@ -17,8 +17,6 @@ */ package org.apache.cassandra.cql3.validation.entities; -import org.apache.cassandra.ServerTestUtils; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.Duration; import org.apache.cassandra.cql3.UntypedResultSet; @@ -41,6 +39,7 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.*; +import static org.apache.cassandra.ServerTestUtils.daemonInitialization; import static org.apache.cassandra.utils.Clock.Global.nanoTime; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -51,13 +50,8 @@ public class JsonTest extends CQLTester @BeforeClass public static void setUpClass() { - ServerTestUtils.daemonInitialization(); - if (ROW_CACHE_SIZE_IN_MIB > 0) - DatabaseDescriptor.setRowCacheSizeInMiB(ROW_CACHE_SIZE_IN_MIB); - + daemonInitialization(); StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - - // Once per-JVM is enough prepareServer(); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java index 38f724968e..086bc88bf2 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java @@ -18,17 +18,18 @@ package org.apache.cassandra.cql3.validation.entities; import java.nio.ByteBuffer; -import java.util.*; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import com.google.common.collect.ImmutableSet; - import org.apache.commons.lang3.StringUtils; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.schema.ColumnMetadata; @@ -44,6 +45,7 @@ import org.apache.cassandra.db.DeletionTime; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.index.IndexNotAvailableException; import org.apache.cassandra.index.SecondaryIndexManager; @@ -58,7 +60,6 @@ import org.apache.cassandra.utils.MD5Digest; import org.apache.cassandra.utils.Pair; import static java.lang.String.format; - import static org.apache.cassandra.Util.throwAssert; import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java index 9ad6aeb047..e2989c6363 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java @@ -22,22 +22,21 @@ import java.util.UUID; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.dht.ByteOrderedPartitioner; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.service.StorageService; +import static org.apache.cassandra.ServerTestUtils.daemonInitialization; + public class UserTypesTest extends CQLTester { @BeforeClass public static void setUpClass() // overrides CQLTester.setUpClass() { - ServerTestUtils.daemonInitialization(); - + daemonInitialization(); // Selecting partitioner for a table is not exposed on CREATE TABLE. StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - prepareServer(); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java index b2da6e59ca..3b9ce8ac80 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java @@ -207,7 +207,7 @@ public class VirtualTableTest extends CQLTester } @BeforeClass - public static void setUpClass() + public static void setUpVirtualTables() { ServerTestUtils.daemonInitialization(); @@ -371,8 +371,6 @@ public class VirtualTableTest extends CQLTester }; VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(vt1, vt2, vt3, vt4, vt5))); - - CQLTester.setUpClass(); } @Test @@ -1076,7 +1074,7 @@ public class VirtualTableTest extends CQLTester } catch (InvalidQueryException ex) { - assertTrue(ex.getMessage().contains("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance")); + assertTrue(ex.getMessage(), ex.getMessage().contains("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance")); } } diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterNTSTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterNTSTest.java index e8f9842007..491e8f436c 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterNTSTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterNTSTest.java @@ -19,21 +19,16 @@ package org.apache.cassandra.cql3.validation.operations; import java.util.List; -import java.util.UUID; import org.junit.Test; import com.datastax.driver.core.PreparedStatement; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.StorageService; import org.assertj.core.api.Assertions; import static org.junit.Assert.assertEquals; @@ -115,34 +110,8 @@ public class AlterNTSTest extends CQLTester { requireAuthentication(); - // Add a peer - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.2")); - - // Register an Endpoint snitch which returns fixed value for data center. - DatabaseDescriptor.setEndpointSnitch(new IEndpointSnitch() - { - public String getRack(InetAddressAndPort endpoint) { return RACK1; } - public String getDatacenter(InetAddressAndPort endpoint) - { - if(endpoint.getHostAddress(false).equalsIgnoreCase("127.0.0.2")) - return "datacenter2"; - return DATA_CENTER; - } - public > C sortedByProximity(InetAddressAndPort address, C addresses) - { - return null; - } - - public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) - { - return 0; - } - - // NOOP - public void gossiperStarting() { } - - public boolean isWorthMergingForRangeQuery(ReplicaCollection merged, ReplicaCollection l1, ReplicaCollection l2) { return false; } - }); + // Add a peer in another DC + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.0.0.2"), DATA_CENTER_REMOTE, RACK1); // try modifying the system_auth keyspace without second DC which has active node. assertInvalidThrow(ConfigurationException.class, "ALTER KEYSPACE system_auth WITH replication = { 'class' : 'NetworkTopologyStrategy', '" + DATA_CENTER + "' : 2 }"); diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java index c2cf8d1631..d21d11bdd3 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/AlterTest.java @@ -17,12 +17,9 @@ */ package org.apache.cassandra.cql3.validation.operations; -import java.util.UUID; - import org.junit.Assert; import org.junit.Test; -import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; @@ -30,16 +27,14 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.memtable.Memtable; import org.apache.cassandra.db.memtable.SkipListMemtable; import org.apache.cassandra.db.memtable.TestMemtable; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.SyntaxException; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.index.StubIndex; import org.apache.cassandra.schema.MemtableParams; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.SchemaKeyspaceTables; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; import static java.lang.String.format; import static org.junit.Assert.assertEquals; @@ -231,6 +226,44 @@ public class AlterTest extends CQLTester row(1, 100, 100, 100, 100)); } + @Test + public void testDropColumnWithIndex() throws Throwable + { + createTable("CREATE TABLE %s(k int, c int, v1 int, v2 int, v3 int, v4 int, PRIMARY KEY (k, c))"); + createIndex(String.format("CREATE CUSTOM INDEX multi_idx ON %%s(v1, v2) USING '%s'", StubIndex.class.getName())); + createIndex("CREATE INDEX v3_idx ON %s(v3)"); + + assertInvalidMessage("Cannot drop column v1 because it has dependent secondary indexes", + "ALTER TABLE %s DROP (v1)"); + assertInvalidMessage("Cannot drop column v2 because it has dependent secondary indexes", + "ALTER TABLE %s DROP (v2)"); + // targets for a multi column custom index cannot be trivially derived from IndexMetadata so for now we + // assume that any such index may be depenendant on all columns + assertInvalidMessage("Cannot drop column v3 because it has dependent secondary indexes", + "ALTER TABLE %s DROP (v3)"); + // current implementation will only complain about 1 of the columns (previous behaviour preserved for now) + assertInvalidMessage("because it has dependent secondary indexes", + "ALTER TABLE %s DROP (v1, v2)"); + } + + @Test + public void testRenameColumnWithIndex() throws Throwable + { + createTable("CREATE TABLE %s(k int, c1 int, c2 int, c3 int, v int, PRIMARY KEY (k, c1, c2, c3))"); + createIndex(String.format("CREATE CUSTOM INDEX multi_idx ON %%s(c1, c2) USING '%s'", StubIndex.class.getName())); + createIndex("CREATE INDEX v3_idx ON %s(c3)"); + + assertInvalidMessage("Cannot rename column c1 because it has dependent secondary indexes (multi_idx)", + "ALTER TABLE %s RENAME c1 TO c99"); + assertInvalidMessage("Cannot rename column c2 because it has dependent secondary indexes (multi_idx)", + "ALTER TABLE %s RENAME c2 TO c99"); + // targets for a multi column custom index cannot be trivially derived from IndexMetadata so for now we + // assume that any such index may be depenendant on all columns + assertInvalidMessage("Cannot rename column c3 because it has dependent secondary indexes", + "ALTER TABLE %s RENAME c3 to v99"); + } + + @Test public void testChangeStrategyWithUnquotedAgrument() throws Throwable @@ -298,15 +331,8 @@ public class AlterTest extends CQLTester @Test public void testCreateAlterNetworkTopologyWithDefaults() throws Throwable { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); - InetAddressAndPort remote = InetAddressAndPort.getByName("127.0.0.4"); - metadata.updateHostId(UUID.randomUUID(), local); - metadata.updateNormalToken(Util.token("A"), local); - metadata.updateHostId(UUID.randomUUID(), remote); - metadata.updateNormalToken(Util.token("B"), remote); - + // Node 1 is added automatically during setup, add another node in a different dc/rack + ClusterMetadataTestHelper.register(4, DATA_CENTER_REMOTE, RACK1); // With two datacenters we should respect anything passed in as a manual override String ks1 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1, '" + DATA_CENTER_REMOTE + "': 3}"); @@ -350,20 +376,18 @@ public class AlterTest extends CQLTester row("tbl1", map("class", "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", "min_threshold", "7", "max_threshold", "32"))); - metadata.clearUnsafe(); } @Test public void testCreateSimpleAlterNTSDefaults() throws Throwable { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); - InetAddressAndPort remote = InetAddressAndPort.getByName("127.0.0.4"); - metadata.updateHostId(UUID.randomUUID(), local); + // Node 1 is added automatically during setup, add another node in a different dc/rack + ClusterMetadataTestHelper.register(4, DATA_CENTER_REMOTE, RACK1); + /* + // todo (rebase) we also need these metadata.updateNormalToken(Util.token("A"), local); - metadata.updateHostId(UUID.randomUUID(), remote); metadata.updateNormalToken(Util.token("B"), remote); + */ // Let's create a keyspace first with SimpleStrategy String ks1 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 2}"); @@ -393,90 +417,80 @@ public class AlterTest extends CQLTester @Test public void testDefaultRF() throws Throwable { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); - InetAddressAndPort remote = InetAddressAndPort.getByName("127.0.0.4"); - metadata.updateHostId(UUID.randomUUID(), local); - metadata.updateNormalToken(Util.token("A"), local); - metadata.updateHostId(UUID.randomUUID(), remote); - metadata.updateNormalToken(Util.token("B"), remote); + // Node 1 is added automatically during setup, add another node in a different dc/rack + ClusterMetadataTestHelper.register(4, DATA_CENTER_REMOTE, RACK1); DatabaseDescriptor.setDefaultKeyspaceRF(3); - //ensure default rf is being taken into account during creation, and user can choose to override the default - String ks1 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy' }"); - String ks2 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 2 }"); - String ks3 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); - String ks4 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 2 }"); + try + { + //ensure default rf is being taken into account during creation, and user can choose to override the default + String ks1 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy' }"); + String ks2 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 2 }"); + String ks3 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); + String ks4 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 2 }"); - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks1, true, map("class","org.apache.cassandra.locator.SimpleStrategy","replication_factor", Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()))), - row(ks2, true, map("class","org.apache.cassandra.locator.SimpleStrategy","replication_factor", "2")), - row(ks3, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, - Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()), DATA_CENTER_REMOTE, Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()))), - row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks1, true, map("class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()))), + row(ks2, true, map("class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", "2")), + row(ks3, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, + Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()), DATA_CENTER_REMOTE, Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()))), + row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); - //ensure alter keyspace does not default to default rf unless altering from NTS to SS - //no change alter - schemaChange("ALTER KEYSPACE " + ks4 + " WITH durable_writes=true"); - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); - schemaChange("ALTER KEYSPACE " + ks4 + " WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); + //ensure alter keyspace does not default to default rf unless altering from NTS to SS + //no change alter + schemaChange("ALTER KEYSPACE " + ks4 + " WITH durable_writes=true"); + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); + schemaChange("ALTER KEYSPACE " + ks4 + " WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks4, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); - // change from SS to NTS - // without specifying RF - schemaChange("ALTER KEYSPACE " + ks2 + " WITH replication={ 'class' : 'NetworkTopologyStrategy' } AND durable_writes=true"); - // verify that RF of SS is retained - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks2, true, map("class","org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); - // with specifying RF - schemaChange("ALTER KEYSPACE " + ks1 + " WITH replication={ 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } AND durable_writes=true"); - // verify that explicitly mentioned RF is taken into account - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks1, true, map("class","org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "1", DATA_CENTER_REMOTE, "1"))); + // change from SS to NTS + // without specifying RF + schemaChange("ALTER KEYSPACE " + ks2 + " WITH replication={ 'class' : 'NetworkTopologyStrategy' } AND durable_writes=true"); + // verify that RF of SS is retained + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks2, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "2", DATA_CENTER_REMOTE, "2"))); + // with specifying RF + schemaChange("ALTER KEYSPACE " + ks1 + " WITH replication={ 'class' : 'NetworkTopologyStrategy', 'replication_factor': '1' } AND durable_writes=true"); + // verify that explicitly mentioned RF is taken into account + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks1, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DATA_CENTER, "1", DATA_CENTER_REMOTE, "1"))); - // change from NTS to SS - // without specifying RF - schemaChange("ALTER KEYSPACE " + ks4 + " WITH replication={ 'class' : 'SimpleStrategy' } AND durable_writes=true"); - // verify that default RF is taken into account - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks4, true, map("class","org.apache.cassandra.locator.SimpleStrategy","replication_factor", Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF())))); - // with specifying RF - schemaChange("ALTER KEYSPACE " + ks3 + " WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : '1' } AND durable_writes=true"); - // verify that explicitly mentioned RF is taken into account - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks3, true, map("class","org.apache.cassandra.locator.SimpleStrategy","replication_factor", "1"))); + // change from NTS to SS + // without specifying RF + schemaChange("ALTER KEYSPACE " + ks4 + " WITH replication={ 'class' : 'SimpleStrategy' } AND durable_writes=true"); + // verify that default RF is taken into account + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks4, true, map("class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF())))); + // with specifying RF + schemaChange("ALTER KEYSPACE " + ks3 + " WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : '1' } AND durable_writes=true"); + // verify that explicitly mentioned RF is taken into account + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks3, true, map("class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", "1"))); - // verify updated default does not effect existing keyspaces - // create keyspaces - String ks5 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy' }"); - String ks6 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); - String oldRF = Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()); - // change default - DatabaseDescriptor.setDefaultKeyspaceRF(2); - // verify RF of existing keyspaces - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks5, true, map("class","org.apache.cassandra.locator.SimpleStrategy","replication_factor", oldRF))); - assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), - row(ks6, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", - DATA_CENTER, oldRF, DATA_CENTER_REMOTE, oldRF))); - - //clean up config change - DatabaseDescriptor.setDefaultKeyspaceRF(1); - - //clean up keyspaces - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks1)); - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks2)); - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks3)); - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks4)); - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks5)); - execute(String.format("DROP KEYSPACE IF EXISTS %s", ks6)); + // verify updated default does not effect existing keyspaces + // create keyspaces + String ks5 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'SimpleStrategy' }"); + String ks6 = createKeyspace("CREATE KEYSPACE %s WITH replication={ 'class' : 'NetworkTopologyStrategy' }"); + String oldRF = Integer.toString(DatabaseDescriptor.getDefaultKeyspaceRF()); + // change default + DatabaseDescriptor.setDefaultKeyspaceRF(2); + // verify RF of existing keyspaces + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks5, true, map("class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", oldRF))); + assertRowsIgnoringOrderAndExtra(execute("SELECT keyspace_name, durable_writes, replication FROM system_schema.keyspaces"), + row(ks6, true, map("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", + DATA_CENTER, oldRF, DATA_CENTER_REMOTE, oldRF))); + } + finally + { + //clean up config change + DatabaseDescriptor.setDefaultKeyspaceRF(1); + } } - /** * Test {@link ConfigurationException} thrown when altering a keyspace to invalid DC option in replication configuration. */ @@ -710,7 +724,6 @@ public class AlterTest extends CQLTester "The 'class' option must not be empty. To disable compression use 'enabled' : false", "ALTER TABLE %s WITH compression = { 'class' : ''};"); - assertAlterTableThrowsException(ConfigurationException.class, "If the 'enabled' option is set to false no other options must be specified", "ALTER TABLE %s WITH compression = { 'enabled' : 'false', 'class' : 'SnappyCompressor'};"); diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/CompactStorageSplit2Test.java b/test/unit/org/apache/cassandra/cql3/validation/operations/CompactStorageSplit2Test.java index 1c4feb3840..aac7bcebcf 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/CompactStorageSplit2Test.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/CompactStorageSplit2Test.java @@ -2001,7 +2001,6 @@ public class CompactStorageSplit2Test extends CQLTester execute("UPDATE %s SET value = ? WHERE partitionKey = ? AND clustering_1 = ?", null, 0, 0); flush(forceFlush); - if (isEmpty(CompactStorageSplit1Test.compactOption)) { assertRows(execute("SELECT * FROM %s WHERE partitionKey = ? AND (clustering_1) IN ((?), (?))", diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java index f671dd9159..0e881dac2d 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/CreateTest.java @@ -17,6 +17,16 @@ */ package org.apache.cassandra.cql3.validation.operations; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.Duration; @@ -30,27 +40,31 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.io.util.DataOutputBuffer; -import org.apache.cassandra.locator.AbstractEndpointSnitch; -import org.apache.cassandra.locator.IEndpointSnitch; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.schema.*; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.schema.MemtableParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SchemaKeyspaceTables; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.Register; import org.apache.cassandra.triggers.ITrigger; import org.apache.cassandra.utils.ByteBufferUtil; -import org.junit.Assert; -import org.junit.Test; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; import static java.lang.String.format; -import static org.apache.cassandra.cql3.Duration.*; -import static org.junit.Assert.*; +import static org.apache.cassandra.cql3.Duration.NANOS_PER_HOUR; +import static org.apache.cassandra.cql3.Duration.NANOS_PER_MICRO; +import static org.apache.cassandra.cql3.Duration.NANOS_PER_MILLI; +import static org.apache.cassandra.cql3.Duration.NANOS_PER_MINUTE; +import static org.apache.cassandra.tcm.membership.MembershipUtils.endpoint; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class CreateTest extends CQLTester { @@ -544,31 +558,12 @@ public class CreateTest extends CQLTester // tests CASSANDRA-4278 public void testHyphenDatacenters() throws Throwable { - IEndpointSnitch snitch = DatabaseDescriptor.getEndpointSnitch(); - - // Register an EndpointSnitch which returns fixed values for test. - DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) { return RACK1; } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) { return "us-east-1"; } - - @Override - public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) { return 0; } - }); - - // this forces the dc above to be added to the list of known datacenters (fixes static init problem + // this forces the dc 'us-east-1' to be added to the list of known datacenters (fixes static init problem // with this group of tests), ok to remove at some point if doing so doesn't break the test - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.255")); + ClusterMetadataService.instance().commit(new Register(new NodeAddresses(endpoint(255)), + new Location("us-east-1", RACK1), + NodeVersion.CURRENT)); execute("CREATE KEYSPACE Foo WITH replication = { 'class' : 'NetworkTopologyStrategy', 'us-east-1' : 1 };"); - - // Restore the previous EndpointSnitch - DatabaseDescriptor.setEndpointSnitch(snitch); - - // clean up - execute("DROP KEYSPACE IF EXISTS Foo"); } @Test @@ -729,6 +724,27 @@ public class CreateTest extends CQLTester + " WITH compression = { 'class' : 'SnappyCompressor', 'unknownOption' : 32 };"); } + @Test + public void testUsingDeterministicTableID() + { + DatabaseDescriptor.useDeterministicTableID(true); + + createTable("CREATE TABLE %s (id text PRIMARY KEY);"); + TableMetadata tmd = currentTableMetadata(); + assertEquals(TableId.unsafeDeterministic(tmd.keyspace, tmd.name), tmd.id); + + } + + @Test + public void testNotUsingDeterministicTableIDWhenDisabled() + { + DatabaseDescriptor.useDeterministicTableID(false); + + createTable("CREATE TABLE %s (id text PRIMARY KEY);"); + TableMetadata tmd = currentTableMetadata(); + assertFalse(TableId.unsafeDeterministic(tmd.keyspace, tmd.name).equals(tmd.id)); + } + private void assertThrowsConfigurationException(String errorMsg, String createStmt) { try diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/DropRecreateAndRestoreTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/DropRecreateAndRestoreTest.java index d385639be8..bdf9448044 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/DropRecreateAndRestoreTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/DropRecreateAndRestoreTest.java @@ -33,6 +33,9 @@ import org.apache.cassandra.schema.TableId; public class DropRecreateAndRestoreTest extends CQLTester { + // don't run CQLTester after test, commitlog is messed up by testCreateWithIdRestore and we now need to commit a ForceSnapshot when resetting the CMS + public void afterTest() {} + @Test public void testCreateWithIdRestore() throws Throwable { diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java index fe4073c19c..93339c384d 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java @@ -43,12 +43,6 @@ import org.apache.cassandra.exceptions.SyntaxException; @RunWith(Parameterized.class) public class InsertUpdateIfConditionCollectionsTest extends CQLTester { - @Parameterized.Parameter(0) - public String clusterMinVersion; - - @Parameterized.Parameter(1) - public Runnable assertion; - @Parameterized.Parameters(name = "{index}: clusterMinVersion={0}") public static Collection data() { @@ -57,6 +51,9 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester return InsertUpdateIfConditionTest.data(); } + @Parameterized.Parameter(0) + public String clusterMinVersion; + @BeforeClass public static void beforeClass() { @@ -66,7 +63,7 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester @Before public void before() { - InsertUpdateIfConditionTest.beforeSetup(clusterMinVersion, assertion); + InsertUpdateIfConditionTest.beforeSetup(clusterMinVersion); } @AfterClass diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionStaticsTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionStaticsTest.java index 3270fc157a..7b670956a0 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionStaticsTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionStaticsTest.java @@ -39,12 +39,6 @@ import org.apache.cassandra.cql3.CQLTester; @RunWith(Parameterized.class) public class InsertUpdateIfConditionStaticsTest extends CQLTester { - @Parameterized.Parameter(0) - public String clusterMinVersion; - - @Parameterized.Parameter(1) - public Runnable assertion; - @Parameterized.Parameters(name = "{index}: clusterMinVersion={0}") public static Collection data() { @@ -52,6 +46,9 @@ public class InsertUpdateIfConditionStaticsTest extends CQLTester return InsertUpdateIfConditionTest.data(); } + @Parameterized.Parameter(0) + public String clusterMinVersion; + @BeforeClass public static void beforeClass() { @@ -61,7 +58,7 @@ public class InsertUpdateIfConditionStaticsTest extends CQLTester @Before public void before() { - InsertUpdateIfConditionTest.beforeSetup(clusterMinVersion, assertion); + InsertUpdateIfConditionTest.beforeSetup(clusterMinVersion); } @AfterClass diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java index c57f61d6eb..1d977a9357 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java @@ -30,18 +30,15 @@ import org.junit.runners.Parameterized; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.Duration; -import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.SchemaKeyspaceTables; -import org.apache.cassandra.utils.CassandraVersion; import static java.lang.String.format; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; /* InsertUpdateIfConditionCollectionsTest class has been split into multiple ones because of timeout issues (CASSANDRA-16670) * Any changes here check if they apply to the other classes @@ -52,50 +49,38 @@ import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) public class InsertUpdateIfConditionTest extends CQLTester { - @Parameterized.Parameter(0) - public String clusterMinVersion; - - @Parameterized.Parameter(1) - public Runnable assertion; - @Parameterized.Parameters(name = "{index}: clusterMinVersion={0}") public static Collection data() { ServerTestUtils.daemonInitialization(); - return Arrays.asList(new Object[]{ "3.0", (Runnable) () -> { - assertTrue(Gossiper.instance.isUpgradingFromVersionLowerThan(new CassandraVersion("3.11"))); - } }, - new Object[]{ "3.11", (Runnable) () -> { - assertTrue(Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION)); - assertFalse(Gossiper.instance.isUpgradingFromVersionLowerThan(new CassandraVersion("3.11"))); - } }, - new Object[]{ SystemKeyspace.CURRENT_VERSION.toString(), (Runnable) () -> { - assertFalse(Gossiper.instance.isUpgradingFromVersionLowerThan(SystemKeyspace.CURRENT_VERSION)); - } }); + // TODO [tcm] we will require upgrading from 4.1 + return Arrays.asList(new Object[]{ "4.1" }, new Object[]{ "4.0" }); } + @Parameterized.Parameter(0) + public String clusterMinVersion; + @BeforeClass public static void beforeClass() { + CassandraRelevantProperties.TCM_ALLOW_TRANSFORMATIONS_DURING_UPGRADES.setBoolean(true); Gossiper.instance.start(0); } @Before public void before() { - beforeSetup(clusterMinVersion, assertion); + beforeSetup(clusterMinVersion); } - public static void beforeSetup(String clusterMinVersion, Runnable assertion) + public static void beforeSetup(String clusterMinVersion) { // setUpgradeFromVersion adds node2 to the Gossiper. On slow CI envs the Gossiper might auto-remove it after some // timeout if it thinks it's a fat client making the test fail. Just retry C18393. Util.spinAssertEquals(Boolean.TRUE, () -> { Util.setUpgradeFromVersion(clusterMinVersion); - assertion.run(); return true; - }, - 5); + }, 5); } @AfterClass diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java index 347bc1d445..5f7e47ead1 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectLimitTest.java @@ -23,23 +23,20 @@ package org.apache.cassandra.cql3.validation.operations; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.ServerTestUtils; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.dht.ByteOrderedPartitioner; import org.apache.cassandra.service.StorageService; +import static org.apache.cassandra.ServerTestUtils.daemonInitialization; + public class SelectLimitTest extends CQLTester { // This method will be ran instead of the CQLTester#setUpClass @BeforeClass public static void setUpClass() { - ServerTestUtils.daemonInitialization(); - + daemonInitialization(); StorageService.instance.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - DatabaseDescriptor.setPartitionerUnsafe(ByteOrderedPartitioner.instance); - prepareServer(); } diff --git a/test/unit/org/apache/cassandra/db/CellSpecTest.java b/test/unit/org/apache/cassandra/db/CellSpecTest.java index b14b74be22..a72a4650e4 100644 --- a/test/unit/org/apache/cassandra/db/CellSpecTest.java +++ b/test/unit/org/apache/cassandra/db/CellSpecTest.java @@ -131,6 +131,7 @@ public class CellSpecTest public static Collection data() { TableMetadata table = TableMetadata.builder("testing", "testing") .addPartitionKeyColumn("pk", BytesType.instance) + .offline() .build(); byte[] rawBytes = { 0, 1, 2, 3, 4, 5, 6 }; diff --git a/test/unit/org/apache/cassandra/db/CleanupTest.java b/test/unit/org/apache/cassandra/db/CleanupTest.java index 5d176859c2..df05591c28 100644 --- a/test/unit/org/apache/cassandra/db/CleanupTest.java +++ b/test/unit/org/apache/cassandra/db/CleanupTest.java @@ -28,32 +28,33 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import com.google.common.collect.Sets; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; import org.apache.cassandra.dht.Range; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.locator.AbstractNetworkTopologySnitch; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.service.reads.range.TokenUpdater; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.utils.Clock.Global.nanoTime; @@ -83,9 +84,12 @@ public class CleanupTest } @BeforeClass - public static void defineSchema() throws ConfigurationException + public static void defineSchema() throws Exception { - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServerNoRegister(); + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.0.0.1"), "DC1", "RC1"); + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.0.0.2"), "DC1", "RC1"); + SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1), @@ -114,18 +118,21 @@ public class CleanupTest SchemaLoader.createKeyspace(KEYSPACE3, KeyspaceParams.nts("DC1", 1), SchemaLoader.standardCFMD(KEYSPACE3, CF_STANDARD3)); + ServerTestUtils.markCMS(); + } + + @Before + public void resetCMS() + { + ServerTestUtils.resetCMS(); } @Test public void testCleanup() throws ExecutionException, InterruptedException, UnknownHostException { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.clearUnsafe(); - tmd.updateNormalToken(token(new byte[]{ 50 }), InetAddressAndPort.getByName("127.0.0.1")); - Keyspace keyspace = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1); - + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), token(new byte[]{ 50 })).update(); // insert data and verify we get it back w/ range query fillCF(cfs, "val", LOOPS); @@ -151,7 +158,6 @@ public class CleanupTest Keyspace keyspace = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_INDEXED1); - // insert data and verify we get it back w/ range query fillCF(cfs, "birthdate", LOOPS); assertEquals(LOOPS, Util.getAll(Util.cmd(cfs).build()).size()); @@ -168,12 +174,12 @@ public class CleanupTest // we don't allow cleanup when the local host has no range to avoid wipping up all data when a node has not join the ring. // So to make sure cleanup erase everything here, we give the localhost the tiniest possible range. - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); byte[] tk1 = new byte[1], tk2 = new byte[1]; tk1[0] = 2; tk2[0] = 1; - tmd.updateNormalToken(new BytesToken(tk1), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(new BytesToken(tk2), InetAddressAndPort.getByName("127.0.0.2")); + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .withTokens(InetAddressAndPort.getByName("127.0.0.2"), new BytesToken(tk2)) + .update(); CompactionManager.instance.performCleanup(cfs, 2); @@ -190,8 +196,6 @@ public class CleanupTest @Test public void testCleanupWithNewToken() throws ExecutionException, InterruptedException, UnknownHostException { - StorageService.instance.getTokenMetadata().clearUnsafe(); - Keyspace keyspace = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1); @@ -199,13 +203,13 @@ public class CleanupTest fillCF(cfs, "val", LOOPS); assertEquals(LOOPS, Util.getAll(Util.cmd(cfs).build()).size()); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); byte[] tk1 = new byte[1], tk2 = new byte[1]; tk1[0] = 2; tk2[0] = 1; - tmd.updateNormalToken(new BytesToken(tk1), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(new BytesToken(tk2), InetAddressAndPort.getByName("127.0.0.2")); + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .withTokens(InetAddressAndPort.getByName("127.0.0.2"), new BytesToken(tk2)) + .update(); CompactionManager.instance.performCleanup(cfs, 2); assertEquals(0, Util.getAll(Util.cmd(cfs).build()).size()); @@ -226,15 +230,13 @@ public class CleanupTest private void testCleanupWithNoTokenRange(boolean isUserDefined) throws Exception { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.clearUnsafe(); - tmd.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.1")); byte[] tk1 = {2}; - tmd.updateNormalToken(new BytesToken(tk1), InetAddressAndPort.getByName("127.0.0.1")); - + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .update(); Keyspace keyspace = Keyspace.open(KEYSPACE2); - keyspace.setMetadata(KeyspaceMetadata.create(KEYSPACE2, KeyspaceParams.nts("DC1", 1))); + KeyspaceMetadata ksm = keyspace.getMetadata().withSwapped(KeyspaceParams.nts("DC1", 1)); + SchemaTestUtil.addOrUpdateKeyspace(ksm, true); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD2); // insert data and verify we get it back w/ range query @@ -242,7 +244,8 @@ public class CleanupTest assertEquals(LOOPS, Util.getAll(Util.cmd(cfs).build()).size()); // remove replication on DC1 - keyspace.setMetadata(KeyspaceMetadata.create(KEYSPACE2, KeyspaceParams.nts("DC1", 0))); + ksm = ksm.withSwapped(KeyspaceParams.nts("DC1", 0)); + SchemaTestUtil.addOrUpdateKeyspace(ksm, true); // clear token range for localhost on DC1 if (isUserDefined) @@ -275,9 +278,10 @@ public class CleanupTest Keyspace keyspace = Keyspace.open(KEYSPACE3); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD3); cfs.disableAutoCompaction(); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.clearUnsafe(); - tmd.updateNormalToken(token(new byte[]{ 50 }), InetAddressAndPort.getByName("127.0.0.1")); + byte[] tk1 = new byte[] { 50 }; + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .update(); + for (byte i = 0; i < 100; i++) { @@ -307,7 +311,9 @@ public class CleanupTest // single token - 127.0.0.1 owns everything, cleanup should be noop cfs.forceCleanup(2); assertEquals(beforeFirstCleanup, cfs.getLiveSSTables()); - tmd.updateNormalToken(token(new byte[]{ 120 }), InetAddressAndPort.getByName("127.0.0.2")); + byte[] tk2 = new byte[] { 120 }; + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.2"), new BytesToken(tk2)) + .update(); cfs.forceCleanup(2); for (SSTableReader sstable : cfs.getLiveSSTables()) @@ -324,8 +330,6 @@ public class CleanupTest @Test public void testuserDefinedCleanupWithNewToken() throws ExecutionException, InterruptedException, UnknownHostException { - StorageService.instance.getTokenMetadata().clearUnsafe(); - Keyspace keyspace = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1); @@ -333,14 +337,13 @@ public class CleanupTest fillCF(cfs, "val", LOOPS); assertEquals(LOOPS, Util.getAll(Util.cmd(cfs).build()).size()); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); byte[] tk1 = new byte[1], tk2 = new byte[1]; tk1[0] = 2; tk2[0] = 1; - tmd.updateNormalToken(new BytesToken(tk1), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(new BytesToken(tk2), InetAddressAndPort.getByName("127.0.0.2")); - + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .withTokens(InetAddressAndPort.getByName("127.0.0.2"), new BytesToken(tk2)) + .update(); for(SSTableReader r: cfs.getLiveSSTables()) CompactionManager.instance.forceUserDefinedCleanup(r.getFilename()); @@ -351,7 +354,6 @@ public class CleanupTest public void testNeedsCleanup() throws Exception { // setup - StorageService.instance.getTokenMetadata().clearUnsafe(); Keyspace keyspace = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF_STANDARD1); fillCF(cfs, "val", LOOPS); diff --git a/test/unit/org/apache/cassandra/db/CleanupTransientTest.java b/test/unit/org/apache/cassandra/db/CleanupTransientTest.java index d611bfa71d..d1a275c657 100644 --- a/test/unit/org/apache/cassandra/db/CleanupTransientTest.java +++ b/test/unit/org/apache/cassandra/db/CleanupTransientTest.java @@ -23,11 +23,11 @@ import java.util.LinkedList; import java.util.List; import java.util.UUID; -import org.apache.cassandra.locator.RangesAtEndpoint; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.compaction.CompactionManager; @@ -36,12 +36,10 @@ import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; -import org.apache.cassandra.locator.AbstractNetworkTopologySnitch; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.service.PendingRangeCalculatorService; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.ByteBufferUtil; @@ -75,7 +73,7 @@ public class CleanupTransientTest DatabaseDescriptor.daemonInitialization(); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); oldPartitioner = StorageService.instance.setPartitionerUnsafe(partitioner); - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServerNoRegister(); SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple("2/1"), SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1), @@ -84,8 +82,6 @@ public class CleanupTransientTest StorageService ss = StorageService.instance; final int RING_SIZE = 2; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); ArrayList endpointTokens = new ArrayList<>(); ArrayList keyTokens = new ArrayList<>(); List hosts = new ArrayList<>(); @@ -94,24 +90,7 @@ public class CleanupTransientTest endpointTokens.add(RandomPartitioner.MINIMUM); endpointTokens.add(RandomPartitioner.instance.midpoint(RandomPartitioner.MINIMUM, new RandomPartitioner.BigIntegerToken(RandomPartitioner.MAXIMUM))); - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - - DatabaseDescriptor.setEndpointSnitch(new AbstractNetworkTopologySnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) - { - return "RC1"; - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - return "DC1"; - } - }); + Util.createInitialRing(endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); } @Test diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreClientModeTest.java b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreClientModeTest.java index af77938fda..bf9f9dc630 100644 --- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreClientModeTest.java +++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreClientModeTest.java @@ -31,15 +31,17 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.locator.SimpleSnitch; +import org.apache.cassandra.schema.DistributedSchema; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaTransformations; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableMetadataRef; import org.apache.cassandra.schema.Types; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import static org.apache.cassandra.cql3.CQLTester.KEYSPACE; import static org.apache.cassandra.cql3.QueryProcessor.parseStatement; @@ -60,10 +62,14 @@ public class ColumnFamilyStoreClientModeTest public static void setUpClass() { DatabaseDescriptor.clientInitialization(); + if (DatabaseDescriptor.getPartitioner() == null) + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + DatabaseDescriptor.setEndpointSnitch(new SimpleSnitch()); DatabaseDescriptor.getRawConfig().memtable_flush_writers = 1; DatabaseDescriptor.getRawConfig().local_system_data_file_directory = tempFolder.toString(); DatabaseDescriptor.getRawConfig().partitioner = "Murmur3Partitioner"; + DatabaseDescriptor.setLocalDataCenter("DC1"); DatabaseDescriptor.applyPartitioner(); } @@ -71,16 +77,15 @@ public class ColumnFamilyStoreClientModeTest public void testTopPartitionsAreNotInitialized() throws IOException { CreateTableStatement.Raw schemaStatement = parseStatement("CREATE TABLE " + KEYSPACE + '.' + TABLE + " (a int, b text, PRIMARY KEY (a))", CreateTableStatement.Raw.class, "CREATE TABLE"); - - Schema.instance.transform(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(KEYSPACE, KeyspaceParams.simple(1)), true)); + KeyspaceMetadata ksm = KeyspaceMetadata.create(KEYSPACE, KeyspaceParams.simple(1)); + DistributedSchema initialSchema = new DistributedSchema(Keyspaces.of(ksm)); + ClusterMetadataService.initializeForClients(initialSchema); + ClusterMetadata.current().schema.initializeKeyspaceInstances(DistributedSchema.empty(), false); Types types = Types.rawBuilder(KEYSPACE).build(); - Schema.instance.transform(SchemaTransformations.addTypes(types, true)); - ClientState state = ClientState.forInternalCalls(KEYSPACE); CreateTableStatement statement = schemaStatement.prepare(state); statement.validate(state); - TableMetadata tableMetadata = statement.builder(types) .id(TableId.fromUUID(UUID.nameUUIDFromBytes(ArrayUtils.addAll(schemaStatement.keyspace().getBytes(), schemaStatement.table().getBytes())))) .partitioner(Murmur3Partitioner.instance) @@ -88,7 +93,7 @@ public class ColumnFamilyStoreClientModeTest Keyspace.setInitialized(); Directories directories = new Directories(tableMetadata, new Directories.DataDirectory[]{ new Directories.DataDirectory(new org.apache.cassandra.io.util.File(tempFolder.newFolder("datadir"))) }); Keyspace ks = Keyspace.openWithoutSSTables(KEYSPACE); - ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(ks, TABLE, TableMetadataRef.forOfflineTools(tableMetadata), directories, false, false, true); + ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(ks, TABLE, TableMetadataRef.forOfflineTools(tableMetadata).get(), directories, false, false); assertNull(cfs.topPartitions); } diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java index 8855ceb0d3..4de0186fea 100644 --- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java +++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java @@ -73,6 +73,7 @@ import org.apache.cassandra.metrics.ClearableHistogram; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.service.snapshot.SnapshotManifest; import org.apache.cassandra.service.snapshot.TableSnapshot; @@ -793,6 +794,12 @@ public class ColumnFamilyStoreTest return false; } + @Override + public boolean shouldSwitch(FlushReason reason, TableMetadata latest) + { + return false; + } + @Override public void metadataUpdated() { diff --git a/test/unit/org/apache/cassandra/db/ColumnsTest.java b/test/unit/org/apache/cassandra/db/ColumnsTest.java index 4c8bcc0e4c..810e26f945 100644 --- a/test/unit/org/apache/cassandra/db/ColumnsTest.java +++ b/test/unit/org/apache/cassandra/db/ColumnsTest.java @@ -27,13 +27,13 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.commitlog.CommitLog; import org.junit.AfterClass; import org.junit.Test; import org.junit.Assert; -import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.marshal.UTF8Type; @@ -50,7 +50,7 @@ public class ColumnsTest { static { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); } diff --git a/test/unit/org/apache/cassandra/db/CounterCacheTest.java b/test/unit/org/apache/cassandra/db/CounterCacheTest.java index 2b743a9f59..094e7a45f7 100644 --- a/test/unit/org/apache/cassandra/db/CounterCacheTest.java +++ b/test/unit/org/apache/cassandra/db/CounterCacheTest.java @@ -20,29 +20,29 @@ package org.apache.cassandra.db; import java.util.Collections; import java.util.concurrent.ExecutionException; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.ColumnMetadata; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.SchemaTestUtil; -import org.apache.cassandra.dht.Bounds; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.utils.ByteBufferUtil; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.CounterColumnType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.exceptions.WriteTimeoutException; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.CacheService; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class CounterCacheTest { diff --git a/test/unit/org/apache/cassandra/db/CounterMutationVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/db/CounterMutationVerbHandlerOutOfRangeTest.java new file mode 100644 index 0000000000..9ae05e3e4e --- /dev/null +++ b/test/unit/org/apache/cassandra/db/CounterMutationVerbHandlerOutOfRangeTest.java @@ -0,0 +1,192 @@ +/* + * 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.db; + +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.Util; +import org.apache.cassandra.db.context.CounterContext; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.BufferCell; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.InvalidRoutingException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.utils.ByteBufferUtil.bytes; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class CounterMutationVerbHandlerOutOfRangeTest +{ + private static final String KEYSPACE = "CounterCacheTest"; + private static final String TABLE = "Counter1"; + + private CounterMutationVerbHandler handler; + private ColumnFamilyStore cfs; + private long startingTotalMetricCount; + private long startingKeyspaceMetricCount; + + @BeforeClass + public static void init() throws Exception + { + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.createKeyspace(KEYSPACE, + KeyspaceParams.simple(1), + SchemaLoader.counterCFMD(KEYSPACE, TABLE)); + ServerTestUtils.markCMS(); + StorageService.instance.unsafeSetInitialized(); + } + + @Before + public void setup() throws Exception + { + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(broadcastAddress, bytesToken(100)); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + + MessagingService.instance().inboundSink.clear(); + MessagingService.instance().outboundSink.clear(); + + handler = new CounterMutationVerbHandler(); + cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE); + cfs.truncateBlocking(); + startingKeyspaceMetricCount = keyspaceMetricValue(); + startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + } + + @Test + public void acceptMutationForNaturalEndpoint() throws Exception + { + int messageId = randomInt(); + int value = randomInt(); + int key = 30; + CounterMutation mutation = mutation(key, value); + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + + // unlike non-counter mutations, we can't verify the response message for a successful write. + // acting as the leader for the mutation, we'll try to forward the writes to the other replicas + // but this will fail as the other node isn't really there. The response message is written + // by the callback to these messages so it will never get sent. So the best we can do is to check + // that the mutation was actually applied locally. When a counter mutation is rejected by the verb + // handler we *can* verify the failure response message is sent. + verifyWrite(key, value); + assertEquals(startingTotalMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount, keyspaceMetricValue()); + } + + @Test + public void acceptMutationForPendingEndpoint() throws Exception + { + // reset ClusterMetadata then join the remote node and partially + // join the localhost one to emulate pending ranges + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + ClusterMetadataTestHelper.register(broadcastAddress); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, bytesToken(100)); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(broadcastAddress)); + + int messageId = randomInt(); + int value = randomInt(); + int key = 50; + CounterMutation mutation = mutation(key, value); + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + verifyWrite(key, value); + assertEquals(startingTotalMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount, keyspaceMetricValue()); + } + + @Test + public void rejectMutationForTokenOutOfRange() throws Exception + { + // reject a mutation for a token the node neither owns nor is pending + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int value = randomInt(); + int key = 200; + CounterMutation mutation = mutation(key, value); + try + { + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + fail("this should now throw exception"); + } + catch (InvalidRoutingException ignore) {} + assertEquals(startingTotalMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount + 1, keyspaceMetricValue()); + } + + private void verifyWrite(int key, int value) + { + ReadCommand read = Util.cmd(cfs, bytes(key)).build(); + ColumnMetadata col = cfs.metadata().getColumn(bytes("val")); + assertEquals((long)value, CounterContext.instance().total(Util.getOnlyRow(read).getCell(col))); + } + + + private static void verifyFailureResponse(ListenableFuture messageSink, int messageId ) throws Exception + { + MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS); + assertEquals(Verb.FAILURE_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertTrue(response.message.payload instanceof RequestFailureReason); + assertEquals(messageId, response.message.id()); + assertEquals(node1, response.to); + } + + private CounterMutation mutation(int key, int columnValue) + { + TableMetadata cfm = Schema.instance.getTableMetadata(KEYSPACE, TABLE); + DecoratedKey dk = cfs.decorateKey(bytes(key)); + ColumnMetadata col = cfs.metadata().getColumn(bytes("val")); + ByteBuffer val = CounterContext.instance().createLocal(columnValue); + Cell counterCell = BufferCell.live(col, FBUtilities.timestampMicros(), val); + Row row = BTreeRow.singleCellRow(cfs.metadata().comparator.make("clustering_1"), counterCell); + PartitionUpdate update = PartitionUpdate.singleRowUpdate(cfm, dk, row); + return new CounterMutation(new Mutation(update), ConsistencyLevel.ANY); + } + + private long keyspaceMetricValue() + { + return cfs.keyspace.metric.outOfRangeTokenWrites.getCount(); + } +} diff --git a/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java b/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java index 9fa8245771..0b25f19d99 100644 --- a/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java +++ b/test/unit/org/apache/cassandra/db/DiskBoundaryManagerTest.java @@ -30,20 +30,27 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.schema.MockSchema; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; @@ -61,12 +68,22 @@ public class DiskBoundaryManagerTest extends CQLTester private List tableDirs; private Path tmpDir; + @BeforeClass + public static void setUpClass() + { + ServerTestUtils.daemonInitialization(); + StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + ClusterMetadataTestHelper.register(FBUtilities.getBroadcastAddressAndPort()); + ClusterMetadataTestHelper.join(FBUtilities.getBroadcastAddressAndPort(), + BootStrapper.getRandomTokens(ClusterMetadata.current(), 10)); + ServerTestUtils.markCMS(); + } + @Before public void setup() throws IOException { DisallowedDirectories.clearUnwritableUnsafe(); - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.updateNormalTokens(BootStrapper.getRandomTokens(metadata, 10), FBUtilities.getBroadcastAddressAndPort()); createTable("create table %s (id int primary key, x text)"); tmpDir = Files.createTempDirectory("DiskBoundaryManagerTest"); datadirs = Lists.newArrayList(new Directories.DataDirectory(new File(tmpDir, "1")), @@ -103,8 +120,11 @@ public class DiskBoundaryManagerTest extends CQLTester @Test public void updateTokensTest() throws UnknownHostException { + //do not use mock to since it will not be invalidated after alter keyspace + DiskBoundaryManager dbm = getCurrentColumnFamilyStore().diskBoundaryManager; DiskBoundaries dbv1 = dbm.getDiskBoundaries(mock); - StorageService.instance.getTokenMetadata().updateNormalTokens(BootStrapper.getRandomTokens(StorageService.instance.getTokenMetadata(), 10), InetAddressAndPort.getByName("127.0.0.10")); + InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0.10"); + UnsafeJoin.unsafeJoin(Register.register(new NodeAddresses(ep)), BootStrapper.getRandomTokens(ClusterMetadata.current(), 10)); DiskBoundaries dbv2 = dbm.getDiskBoundaries(mock); assertFalse(dbv1.equals(dbv2)); } @@ -132,7 +152,7 @@ public class DiskBoundaryManagerTest extends CQLTester pps.add(pp(200)); pps.add(pp(Long.MAX_VALUE)); // last position is always the max token - DiskBoundaries diskBoundaries = new DiskBoundaries(mock, dirs.getWriteableLocations(), pps, 0, 0); + DiskBoundaries diskBoundaries = new DiskBoundaries(mock, dirs.getWriteableLocations(), pps, Epoch.EMPTY, 0); Assert.assertEquals(Lists.newArrayList(datadirs.get(0)), diskBoundaries.getDisksInBounds(dk(10), dk(50))); Assert.assertEquals(Lists.newArrayList(datadirs.get(2)), diskBoundaries.getDisksInBounds(dk(250), dk(500))); @@ -209,7 +229,7 @@ public class DiskBoundaryManagerTest extends CQLTester { MockCFS(ColumnFamilyStore cfs, Directories dirs) { - super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata, dirs, false, false, true); + super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata.get(), dirs, false, false); } } } diff --git a/test/unit/org/apache/cassandra/db/ImportTest.java b/test/unit/org/apache/cassandra/db/ImportTest.java index 4a1bd79ce4..5cd0f15bdb 100644 --- a/test/unit/org/apache/cassandra/db/ImportTest.java +++ b/test/unit/org/apache/cassandra/db/ImportTest.java @@ -45,11 +45,13 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.PathUtils; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.service.CacheService; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -270,8 +272,6 @@ public class ImportTest extends CQLTester @Test public void testGetCorrectDirectory() throws Throwable { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.updateNormalTokens(BootStrapper.getRandomTokens(metadata, 10), FBUtilities.getBroadcastAddressAndPort()); createTable("create table %s (id int primary key, d int)"); getCurrentColumnFamilyStore().disableAutoCompaction(); @@ -412,38 +412,35 @@ public class ImportTest extends CQLTester getCurrentColumnFamilyStore().clearUnsafe(); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.2")); - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.3")); + InetAddressAndPort ep1 = InetAddressAndPort.getByName("127.0.0.1"); + InetAddressAndPort ep2 = InetAddressAndPort.getByName("127.0.0.2"); + InetAddressAndPort ep3 = InetAddressAndPort.getByName("127.0.0.3"); + // ep1 is registered during fixture setup + assertEquals(NodeState.JOINED, ClusterMetadata.current().directory.peerState(ep1)); + UnsafeJoin.unsafeJoin(Register.register(new NodeAddresses(ep2)), + BootStrapper.getRandomTokens(ClusterMetadata.current(), 5)); + UnsafeJoin.unsafeJoin(Register.register(new NodeAddresses(ep3)), + BootStrapper.getRandomTokens(ClusterMetadata.current(), 5)); File backupdir = moveToBackupDir(sstables); - try - { - SSTableImporter.Options options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(true).verifyTokens(true).build(); - SSTableImporter importer = new SSTableImporter(getCurrentColumnFamilyStore()); - List failed = importer.importNewSSTables(options); - assertEquals(Collections.singletonList(backupdir.toString()), failed); - // verify that we check the tokens if verifySSTables == false but verifyTokens == true: - options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(false).verifyTokens(true).build(); - importer = new SSTableImporter(getCurrentColumnFamilyStore()); - failed = importer.importNewSSTables(options); - assertEquals(Collections.singletonList(backupdir.toString()), failed); + SSTableImporter.Options options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(true).verifyTokens(true).build(); + SSTableImporter importer = new SSTableImporter(getCurrentColumnFamilyStore()); + List failed = importer.importNewSSTables(options); + assertEquals(Collections.singletonList(backupdir.toString()), failed); - // and that we can import with it disabled: - options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(true).verifyTokens(false).build(); - importer = new SSTableImporter(getCurrentColumnFamilyStore()); - failed = importer.importNewSSTables(options); - assertTrue(failed.isEmpty()); + // verify that we check the tokens if verifySSTables == false but verifyTokens == true: + options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(false).verifyTokens(true).build(); + importer = new SSTableImporter(getCurrentColumnFamilyStore()); + failed = importer.importNewSSTables(options); + assertEquals(Collections.singletonList(backupdir.toString()), failed); - } - finally - { - tmd.clearUnsafe(); - } + // and that we can import with it disabled: + options = SSTableImporter.Options.options(backupdir.toString()).verifySSTables(true).verifyTokens(false).build(); + importer = new SSTableImporter(getCurrentColumnFamilyStore()); + failed = importer.importNewSSTables(options); + assertTrue(failed.isEmpty()); } @Test @@ -456,29 +453,24 @@ public class ImportTest extends CQLTester Set sstables = getCurrentColumnFamilyStore().getLiveSSTables(); getCurrentColumnFamilyStore().clearUnsafe(); + InetAddressAndPort ep1 = InetAddressAndPort.getByName("127.0.0.1"); + InetAddressAndPort ep2 = InetAddressAndPort.getByName("127.0.0.2"); + InetAddressAndPort ep3 = InetAddressAndPort.getByName("127.0.0.3"); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.2")); - tmd.updateNormalTokens(BootStrapper.getRandomTokens(tmd, 5), InetAddressAndPort.getByName("127.0.0.3")); - - + // ep1 is registered during fixture setup + assertEquals(NodeState.JOINED, ClusterMetadata.current().directory.peerState(ep1)); + UnsafeJoin.unsafeJoin(Register.register(new NodeAddresses(ep2)), + BootStrapper.getRandomTokens(ClusterMetadata.current(), 5)); + UnsafeJoin.unsafeJoin(Register.register(new NodeAddresses(ep3)), + BootStrapper.getRandomTokens(ClusterMetadata.current(), 5)); File backupdir = moveToBackupDir(sstables); - try - { - SSTableImporter.Options options = SSTableImporter.Options.options(backupdir.toString()) - .verifySSTables(true) - .verifyTokens(true) - .extendedVerify(true).build(); - SSTableImporter importer = new SSTableImporter(getCurrentColumnFamilyStore()); - List failedDirectories = importer.importNewSSTables(options); - assertEquals(Collections.singletonList(backupdir.toString()), failedDirectories); - } - finally - { - tmd.clearUnsafe(); - } + SSTableImporter.Options options = SSTableImporter.Options.options(backupdir.toString()) + .verifySSTables(true) + .verifyTokens(true) + .extendedVerify(true).build(); + SSTableImporter importer = new SSTableImporter(getCurrentColumnFamilyStore()); + List failedDirectories = importer.importNewSSTables(options); + assertEquals(Collections.singletonList(backupdir.toString()), failedDirectories); } @@ -714,7 +706,7 @@ public class ImportTest extends CQLTester { public MockCFS(ColumnFamilyStore cfs, Directories dirs) { - super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata, dirs, false, false, true); + super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata.get(), dirs, false, false); } } } diff --git a/test/unit/org/apache/cassandra/db/KeyspaceTest.java b/test/unit/org/apache/cassandra/db/KeyspaceTest.java index 55baba8469..84c5b3ecfd 100644 --- a/test/unit/org/apache/cassandra/db/KeyspaceTest.java +++ b/test/unit/org/apache/cassandra/db/KeyspaceTest.java @@ -19,36 +19,29 @@ package org.apache.cassandra.db; import java.nio.ByteBuffer; -import java.util.Collection; +import java.util.*; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.apache.cassandra.Util; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.UntypedResultSet; -import org.apache.cassandra.db.compaction.CompactionManager; -import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; -import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.filter.DataLimits; -import org.apache.cassandra.db.filter.RowFilter; -import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.io.sstable.AbstractRowIndexEntry; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.big.BigTableReader; import org.apache.cassandra.metrics.ClearableHistogram; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; -import org.assertj.core.api.Assertions; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; public class KeyspaceTest extends CQLTester { @@ -525,7 +518,7 @@ public class KeyspaceTest extends CQLTester { String ksName = "MissingKeyspace"; - Assertions.assertThatThrownBy(() -> Keyspace.open(ksName, Schema.instance, false)) + Assertions.assertThatThrownBy(() -> Keyspace.open(ksName)) .isInstanceOf(AssertionError.class) .hasMessage("Unknown keyspace " + ksName); } diff --git a/test/unit/org/apache/cassandra/db/MutationVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/db/MutationVerbHandlerOutOfRangeTest.java new file mode 100644 index 0000000000..0421e26aa5 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/MutationVerbHandlerOutOfRangeTest.java @@ -0,0 +1,221 @@ +/* + * 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.db; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.Util; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.BufferCell; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.InvalidRoutingException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.utils.ByteBufferUtil.bytes; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class MutationVerbHandlerOutOfRangeTest +{ + private static final String TEST_NAME = "mutation_vh_test_"; + private static final String KEYSPACE = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + + private ColumnFamilyStore cfs; + private long startingTotalMetricCount; + private long startingKeyspaceMetricCount; + + @BeforeClass + public static void init() throws Exception + { + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.schemaDefinition(TEST_NAME); + ServerTestUtils.markCMS(); + StorageService.instance.unsafeSetInitialized(); + } + + @Before + public void setup() throws Exception + { + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(broadcastAddress, bytesToken(100)); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + + MessagingService.instance().inboundSink.clear(); + MessagingService.instance().outboundSink.clear(); + + cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE); + startingKeyspaceMetricCount = keyspaceMetricValue(cfs); + startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + } + + @Test + public void acceptMutationForNaturalEndpoint() throws Exception + { + acceptMutationForNaturalEndpoint(new MutationVerbHandler()); + } + + @Test + public void acceptReadRepairForNaturalEndpoint() throws Exception + { + acceptMutationForNaturalEndpoint(new ReadRepairVerbHandler()); + } + + private void acceptMutationForNaturalEndpoint(IVerbHandler handler) throws Exception + { + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int value = randomInt(); + int key = 50; + Mutation mutation = mutation(key, value); + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, messageId, key, value, false); + } + + @Test + public void acceptMutationForPendingEndpoint() throws Exception + { + acceptMutationForPendingEndpoint(new MutationVerbHandler()); + } + + @Test + public void acceptReadRepairForPendingEndpoint() throws Exception + { + acceptMutationForPendingEndpoint(new ReadRepairVerbHandler()); + } + + private void acceptMutationForPendingEndpoint(IVerbHandler handler ) throws Exception + { + // reset ClusterMetadata then join the remote node and partially + // join the localhost one to emulate pending ranges + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + ClusterMetadataTestHelper.register(broadcastAddress); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, bytesToken(100)); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(broadcastAddress)); + + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int value = randomInt(); + int key = 50; + Mutation mutation = mutation(key, value); + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, messageId, key, value, false); + } + + @Test + public void rejectMutationForTokenOutOfRange() throws Exception + { + rejectMutationForTokenOutOfRange(new MutationVerbHandler()); + } + + @Test + public void rejectReadRepairForTokenOutOfRange() throws Exception + { + rejectMutationForTokenOutOfRange(new ReadRepairVerbHandler()); + } + + private void rejectMutationForTokenOutOfRange(IVerbHandler handler) throws Exception + { + // reject a mutation for a token the node neither owns nor is pending + int messageId = randomInt(); + int value = randomInt(); + int key = 200; + Mutation mutation = mutation(key, value); + try + { + // note that the failure response is now sent by the InboundSink, so we can't use getAndVerifyResponse + handler.doVerb(Message.builder(Verb.MUTATION_REQ, mutation).from(node1).withId(messageId).build()); + fail("mutation verb handler now throws exception"); + } + catch (InvalidRoutingException ignore) + { + assertEquals(startingTotalMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount + 1, keyspaceMetricValue(cfs)); + } + } + + static int toInt(Cell cell) + { + return cell.accessor().toInt(cell.value()); + } + + private void getAndVerifyResponse(ListenableFuture messageSink, + int messageId, + int key, + int value, + boolean isOutOfRange) throws InterruptedException, ExecutionException, TimeoutException + { + MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS); + assertEquals(isOutOfRange ? Verb.FAILURE_RSP : Verb.MUTATION_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertEquals(isOutOfRange, response.message.payload instanceof RequestFailureReason); + assertEquals(messageId, response.message.id()); + assertEquals(node1, response.to); + assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue(cfs)); + if (!isOutOfRange) + { + ReadCommand read = Util.cmd(cfs, bytes(key)).build(); + ColumnMetadata col = cfs.metadata().getColumn(bytes("v1")); + assertEquals(value, toInt(Util.getOnlyRow(read).getCell(col))); + } + } + + private static long keyspaceMetricValue(ColumnFamilyStore cfs) + { + return cfs.keyspace.metric.outOfRangeTokenWrites.getCount(); + } + + private Mutation mutation(int key, int columnValue) + { + TableMetadata cfm = Schema.instance.getTableMetadata(KEYSPACE, TABLE); + DecoratedKey dk = cfs.decorateKey(bytes(key)); + ColumnMetadata col = cfs.metadata().getColumn(bytes("v1")); + Cell cell = BufferCell.live(col, FBUtilities.timestampMicros(), bytes(columnValue)); + Row row = BTreeRow.singleCellRow(Clustering.EMPTY, cell); + PartitionUpdate update = PartitionUpdate.singleRowUpdate(cfm, dk, row); + return new Mutation(update); + } +} diff --git a/test/unit/org/apache/cassandra/db/ReadCommandTest.java b/test/unit/org/apache/cassandra/db/ReadCommandTest.java index 46eb5e7078..8318aa9d6c 100644 --- a/test/unit/org/apache/cassandra/db/ReadCommandTest.java +++ b/test/unit/org/apache/cassandra/db/ReadCommandTest.java @@ -931,7 +931,10 @@ public class ReadCommandTest { TableParams newParams = cfs.metadata().params.unbuild().gcGraceSeconds(gcGrace).build(); KeyspaceMetadata keyspaceMetadata = Schema.instance.getKeyspaceMetadata(cfs.metadata().keyspace); - SchemaTestUtil.addOrUpdateKeyspace(keyspaceMetadata.withSwapped(keyspaceMetadata.tables.withSwapped(cfs.metadata().withSwapped(newParams))), true); + SchemaTestUtil.addOrUpdateKeyspace( + keyspaceMetadata.withSwapped( + keyspaceMetadata.tables.withSwapped( + cfs.metadata().withSwapped(newParams))), true); } private long getAndResetOverreadCount(ColumnFamilyStore cfs) diff --git a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java new file mode 100644 index 0000000000..3fd3c98e3e --- /dev/null +++ b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java @@ -0,0 +1,272 @@ +/* + * 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.db; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.DataLimits; +import org.apache.cassandra.db.filter.RowFilter; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.InvalidRoutingException; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.net.Verb.READ_REQ; +import static org.junit.Assert.assertEquals; + +public class ReadCommandVerbHandlerOutOfRangeTest +{ + private static ReadCommandVerbHandler handler; + private static TableMetadata metadata_nonreplicated; + private ColumnFamilyStore cfs; + private long startingTotalMetricCount; + private long startingKeyspaceMetricCount; + + private static final String TEST_NAME = "read_command_vh_test_"; + private static final String KEYSPACE_NONREPLICATED = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + + @BeforeClass + public static void init() throws Throwable + { + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.schemaDefinition(TEST_NAME); + ServerTestUtils.markCMS(); + metadata_nonreplicated = Schema.instance.getTableMetadata(KEYSPACE_NONREPLICATED, TABLE); + StorageService.instance.unsafeSetInitialized(); + } + + @Before + public void setup() + { + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(broadcastAddress, bytesToken(100)); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + + MessagingService.instance().inboundSink.clear(); + MessagingService.instance().outboundSink.clear(); + MessagingService.instance().outboundSink.add((message, to) -> false); + MessagingService.instance().inboundSink.add((message) -> false); + + cfs = Keyspace.open(KEYSPACE_NONREPLICATED).getColumnFamilyStore(TABLE); + startingKeyspaceMetricCount = keyspaceMetricValue(cfs); + startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + handler = new ReadCommandVerbHandler(); + } + + private static DecoratedKey key(TableMetadata metadata, int key) + { + return metadata.partitioner.decorateKey(ByteBufferUtil.bytes(key)); + } + + @Test + public void acceptPartitionReadForNaturalEndpoint() throws Exception + { + verifyPartitionReadAccepted(); + } + + @Test + public void acceptPartitionReadForPendingEndpoint() throws Exception + { + // reset ClusterMetadata then join the remote node and partially + // join the localhost one to emulate pending ranges + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + ClusterMetadataTestHelper.register(broadcastAddress); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, bytesToken(100)); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(broadcastAddress)); + + verifyPartitionReadAccepted(); + } + + private void verifyPartitionReadAccepted() throws Exception + { + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int key = 50; + ReadCommand command = partitionRead(key); + handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, messageId, false); + } + + @Test(expected = InvalidRoutingException.class) + public void rejectPartitionReadForTokenOutOfRange() + { + // reject a read for a key who's token the node doesn't own the range for + int messageId = randomInt(); + int key = 200; + ReadCommand command = partitionRead(key); + handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build()); + // we automatically send a failure response if doVerb above throws + } + + @Test + public void acceptRangeReadForNaturalEndpoint() throws Exception + { + // reset ClusterMetadata then join the remote node and partially + // join the localhost one to emulate pending ranges + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + ClusterMetadataTestHelper.register(broadcastAddress); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, bytesToken(100)); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(broadcastAddress)); + + verifyRangeReadAccepted(); + } + + @Test + public void acceptRangeReadForPendingEndpoint() throws Exception + { + verifyRangeReadAccepted(); + } + + private void verifyRangeReadAccepted() throws Exception + { + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + ReadCommand command = rangeRead(50, 60); + handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, messageId, false); + } + + @Test(expected = InvalidRoutingException.class) + public void rejectRangeReadForUnownedRange() throws Exception + { + int messageId = randomInt(); + ReadCommand command = rangeRead(150, 160); + handler.doVerb(Message.builder(READ_REQ, command).from(node1).withId(messageId).build()); + } + + private void getAndVerifyResponse(ListenableFuture messageSink, + int messageId, + boolean isOutOfRange) throws InterruptedException, ExecutionException, TimeoutException + { + MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS); + assertEquals(isOutOfRange ? Verb.FAILURE_RSP : Verb.READ_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertEquals(isOutOfRange, response.message.payload instanceof RequestFailureReason); + assertEquals(messageId, response.message.id()); + assertEquals(node1, response.to); + assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue(cfs)); + } + + private ReadCommand partitionRead(int key) + { + return new StubReadCommand(key, metadata_nonreplicated); + } + + private ReadCommand rangeRead(int start, int end) + { + Range range = new Range<>(key(metadata_nonreplicated, start).getToken(), + key(metadata_nonreplicated, end).getToken()); + return new StubRangeReadCommand(range, metadata_nonreplicated); + } + + private static class StubReadCommand extends SinglePartitionReadCommand + { + private final TableMetadata tmd; + + StubReadCommand(int key, TableMetadata tmd) + { + super(tmd.epoch, + false, + 0, + false, + tmd, + FBUtilities.nowInSeconds(), + ColumnFilter.all(tmd), + RowFilter.NONE, + DataLimits.NONE, + key(tmd, key), + null, + null, + false); + + this.tmd = tmd; + } + + public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController) + { + return EmptyIterators.unfilteredPartition(tmd); + } + + public String toString() + { + return "<>"; + } + } + + private static class StubRangeReadCommand extends PartitionRangeReadCommand + { + private final TableMetadata cfm; + + StubRangeReadCommand(Range range, TableMetadata tmd) + { + super(tmd.epoch, + false, + 0, + false, + tmd, + FBUtilities.nowInSeconds(), + ColumnFilter.all(tmd), + RowFilter.NONE, + DataLimits.NONE, + DataRange.forTokenRange(range), + null, + false); + + this.cfm = tmd; + } + + public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController) + { + return EmptyIterators.unfilteredPartition(cfm); + } + } + + private static long keyspaceMetricValue(ColumnFamilyStore cfs) + { + return cfs.keyspace.metric.outOfRangeTokenReads.getCount(); + } +} diff --git a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java index 5b54646f9e..0e925880ef 100644 --- a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java +++ b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java @@ -19,13 +19,16 @@ package org.apache.cassandra.db; import java.net.UnknownHostException; +import java.util.Collections; import java.util.Random; +import java.util.UUID; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; @@ -33,18 +36,20 @@ import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessageFlag; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.ParamType; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; -import static org.apache.cassandra.net.Verb.*; +import static org.apache.cassandra.net.Verb.READ_REQ; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -66,16 +71,20 @@ public class ReadCommandVerbHandlerTest @BeforeClass public static void init() throws Throwable { - SchemaLoader.loadSchema(); + ServerTestUtils.prepareServerNoRegister(); SchemaLoader.schemaDefinition(TEST_NAME); metadata = Schema.instance.getTableMetadata(KEYSPACE, TABLE); metadata_with_transient = Schema.instance.getTableMetadata(KEYSPACE_WITH_TRANSIENT, TABLE); KEY = key(metadata, 1); - - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.updateNormalToken(KEY.getToken(), InetAddressAndPort.getByName("127.0.0.2")); - tmd.updateNormalToken(key(metadata, 2).getToken(), InetAddressAndPort.getByName("127.0.0.3")); - tmd.updateNormalToken(key(metadata, 3).getToken(), FBUtilities.getBroadcastAddressAndPort()); + InetAddressAndPort ep1 = InetAddressAndPort.getByName("127.0.0.2"); + InetAddressAndPort ep2 = InetAddressAndPort.getByName("127.0.0.3"); + InetAddressAndPort ep3 = FBUtilities.getBroadcastAddressAndPort(); + NodeId node1 = Register.register(new NodeAddresses(UUID.randomUUID(), ep1, ep1, ep1)); + NodeId node2 = Register.register(new NodeAddresses(UUID.randomUUID(), ep2, ep2, ep2)); + NodeId node3 = Register.register(new NodeAddresses(UUID.randomUUID(), ep3, ep3, ep3)); + UnsafeJoin.unsafeJoin(node1, Collections.singleton(KEY.getToken())); + UnsafeJoin.unsafeJoin(node2, Collections.singleton(key(metadata, 2).getToken())); + UnsafeJoin.unsafeJoin(node3, Collections.singleton(key(metadata, 3).getToken())); } @Before @@ -160,7 +169,8 @@ public class ReadCommandVerbHandlerTest TrackingSinglePartitionReadCommand(TableMetadata metadata) { - super(false, + super(metadata.epoch, + false, 0, false, metadata, diff --git a/test/unit/org/apache/cassandra/db/ReadResponseTest.java b/test/unit/org/apache/cassandra/db/ReadResponseTest.java index 7241d89e61..fdc833e3c0 100644 --- a/test/unit/org/apache/cassandra/db/ReadResponseTest.java +++ b/test/unit/org/apache/cassandra/db/ReadResponseTest.java @@ -23,14 +23,17 @@ import java.nio.ByteBuffer; import java.util.Random; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.net.MessagingService; @@ -48,10 +51,18 @@ public class ReadResponseTest private final Random random = new Random(); private TableMetadata metadata; + @BeforeClass + public static void beforeClass() + { + DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); + } @Before public void setup() { + metadata = TableMetadata.builder("ks", "t1") + .offline() .addPartitionKeyColumn("p", Int32Type.instance) .addRegularColumn("v", Int32Type.instance) .partitioner(Murmur3Partitioner.instance) @@ -240,7 +251,8 @@ public class ReadResponseTest { StubReadCommand(int key, TableMetadata metadata, boolean isDigest) { - super(isDigest, + super(metadata.epoch, + isDigest, 0, false, metadata, diff --git a/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java b/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java index acca6edc4a..c9c8ac158b 100644 --- a/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java +++ b/test/unit/org/apache/cassandra/db/RecoveryManagerFlushedTest.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.UUID; import org.junit.Before; import org.junit.BeforeClass; @@ -30,7 +29,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,19 +36,18 @@ import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; -import org.apache.cassandra.io.compress.ZstdCompressor; -import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.compress.DeflateCompressor; import org.apache.cassandra.io.compress.LZ4Compressor; import org.apache.cassandra.io.compress.SnappyCompressor; +import org.apache.cassandra.io.compress.ZstdCompressor; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.security.EncryptionContextGenerator; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; @RunWith(Parameterized.class) public class RecoveryManagerFlushedTest @@ -89,8 +86,6 @@ public class RecoveryManagerFlushedTest public static void defineSchema() throws ConfigurationException { SchemaLoader.prepareServer(); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), FBUtilities.getBroadcastAddressAndPort()); - SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1), @@ -101,12 +96,9 @@ public class RecoveryManagerFlushedTest /* test that commit logs do not replay flushed data */ public void testWithFlush() throws Exception { - // Flush everything that may be in the commit log now to start fresh - Util.flushKeyspace(SchemaConstants.SYSTEM_KEYSPACE_NAME); - Util.flushKeyspace(SchemaConstants.SCHEMA_KEYSPACE_NAME); - - CompactionManager.instance.disableAutoCompaction(); + for (String ks : Schema.instance.getKeyspaces()) + Util.flush(Keyspace.open(ks)); // add a row to another CF so we test skipping mutations within a not-entirely-flushed CF insertRow("Standard2", "key"); @@ -120,7 +112,10 @@ public class RecoveryManagerFlushedTest Keyspace keyspace1 = Keyspace.open(KEYSPACE1); ColumnFamilyStore cfs = keyspace1.getColumnFamilyStore("Standard1"); logger.debug("forcing flush"); + // Flush everything that may be in the commit log now to start fresh Util.flush(cfs); + // Flush system keyspace again because of sstable activity mutation called by the tidier + Util.flushKeyspace(SchemaConstants.SYSTEM_KEYSPACE_NAME); logger.debug("begin manual replay"); // replay the commit log (nothing on Standard1 should be replayed since everything was flushed, so only the row on Standard2 diff --git a/test/unit/org/apache/cassandra/db/RepairedDataInfoTest.java b/test/unit/org/apache/cassandra/db/RepairedDataInfoTest.java index ecf07cceca..43eebd19c1 100644 --- a/test/unit/org/apache/cassandra/db/RepairedDataInfoTest.java +++ b/test/unit/org/apache/cassandra/db/RepairedDataInfoTest.java @@ -26,7 +26,7 @@ import java.util.stream.IntStream; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.schema.MockSchema; @@ -67,7 +67,7 @@ public class RepairedDataInfoTest @BeforeClass public static void setUp() { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); MockSchema.cleanup(); String ks = "repaired_data_info_test"; diff --git a/test/unit/org/apache/cassandra/db/RowCacheTest.java b/test/unit/org/apache/cassandra/db/RowCacheTest.java index 4b37e69414..ec5e6176c1 100644 --- a/test/unit/org/apache/cassandra/db/RowCacheTest.java +++ b/test/unit/org/apache/cassandra/db/RowCacheTest.java @@ -28,34 +28,41 @@ import com.google.common.collect.Lists; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; + import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.cache.RowCacheKey; -import org.apache.cassandra.db.marshal.ValueAccessors; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.db.marshal.AsciiType; -import org.apache.cassandra.db.rows.*; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.marshal.AsciiType; import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.ValueAccessors; import org.apache.cassandra.db.partitions.CachedPartition; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.ClearableHistogram; import org.apache.cassandra.schema.CachingParams; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.service.reads.range.TokenUpdater; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_ORG_CAFFINITAS_OHC_SEGMENTCOUNT; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class RowCacheTest { @@ -292,7 +299,7 @@ public class RowCacheTest @Test public void testRowCacheCleanup() throws Exception { - StorageService.instance.initServer(0); + StorageService.instance.initServer(); CacheService.instance.setRowCacheCapacityInMB(1); rowCacheLoad(100, Integer.MAX_VALUE, 1000); @@ -300,12 +307,12 @@ public class RowCacheTest assertEquals(CacheService.instance.rowCache.size(), 100); store.cleanupCache(); assertEquals(CacheService.instance.rowCache.size(), 100); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); byte[] tk1, tk2; tk1 = "key1000".getBytes(); tk2 = "key1050".getBytes(); - tmd.updateNormalToken(new BytesToken(tk1), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(new BytesToken(tk2), InetAddressAndPort.getByName("127.0.0.2")); + new TokenUpdater().withTokens(InetAddressAndPort.getByName("127.0.0.1"), new BytesToken(tk1)) + .withTokens(InetAddressAndPort.getByName("127.0.0.2"), new BytesToken(tk2)) + .update(); store.cleanupCache(); assertEquals(50, CacheService.instance.rowCache.size()); CacheService.instance.setRowCacheCapacityInMB(0); @@ -314,7 +321,7 @@ public class RowCacheTest @Test public void testInvalidateRowCache() throws Exception { - StorageService.instance.initServer(0); + StorageService.instance.initServer(); CacheService.instance.setRowCacheCapacityInMB(1); rowCacheLoad(100, Integer.MAX_VALUE, 1000); diff --git a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java index 41244ca443..3db0fadf4b 100644 --- a/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java +++ b/test/unit/org/apache/cassandra/db/SchemaCQLHelperTest.java @@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import com.fasterxml.jackson.databind.JsonNode; @@ -31,7 +30,6 @@ import org.apache.cassandra.*; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.marshal.*; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.schema.*; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; @@ -53,12 +51,6 @@ import static org.junit.Assert.assertThat; public class SchemaCQLHelperTest extends CQLTester { - @Before - public void defineSchema() throws ConfigurationException - { - SchemaLoader.prepareServer(); - } - @Test public void testUserTypesCQL() { diff --git a/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java b/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java index c13ef60018..40812b5da1 100644 --- a/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java +++ b/test/unit/org/apache/cassandra/db/SecondaryIndexTest.java @@ -20,7 +20,10 @@ package org.apache.cassandra.db; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -28,16 +31,20 @@ import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; + import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.cql3.Operator; import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.partitions.*; +import org.apache.cassandra.db.partitions.FilteredPartition; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.Index; +import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaTestUtil; diff --git a/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java b/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java index c65258c347..93fe4d3dfb 100644 --- a/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java +++ b/test/unit/org/apache/cassandra/db/SinglePartitionSliceCommandTest.java @@ -39,6 +39,7 @@ import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; @@ -68,7 +69,6 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; diff --git a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java index 0bade4bee8..bcaefc23d9 100644 --- a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java +++ b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java @@ -24,6 +24,7 @@ import java.util.*; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.UntypedResultSet; @@ -53,6 +54,7 @@ public class SystemKeyspaceTest { DatabaseDescriptor.daemonInitialization(); CommitLog.instance.start(); + SchemaLoader.prepareServer(); } @Test @@ -61,7 +63,7 @@ public class SystemKeyspaceTest // Remove all existing tokens Collection current = SystemKeyspace.loadTokens().asMap().get(FBUtilities.getLocalAddressAndPort()); if (current != null && !current.isEmpty()) - SystemKeyspace.updateTokens(current); + SystemKeyspace.updateLocalTokens(current); List tokens = new ArrayList() {{ @@ -69,7 +71,7 @@ public class SystemKeyspaceTest add(new BytesToken(ByteBufferUtil.bytes(String.format("token%d", i)))); }}; - SystemKeyspace.updateTokens(tokens); + SystemKeyspace.updateLocalTokens(tokens); int count = 0; for (Token tok : SystemKeyspace.getSavedTokens()) @@ -87,14 +89,6 @@ public class SystemKeyspaceTest assert !SystemKeyspace.loadTokens().containsValue(token); } - @Test - public void testLocalHostID() - { - UUID firstId = SystemKeyspace.getOrInitializeLocalHostId(); - UUID secondId = SystemKeyspace.getOrInitializeLocalHostId(); - assert firstId.equals(secondId) : String.format("%s != %s%n", firstId.toString(), secondId.toString()); - } - private void assertDeleted() { assertTrue(getSystemSnapshotFiles(SchemaConstants.SYSTEM_KEYSPACE_NAME).isEmpty()); diff --git a/test/unit/org/apache/cassandra/db/TopPartitionTrackerTest.java b/test/unit/org/apache/cassandra/db/TopPartitionTrackerTest.java index 417bda0f58..ef352401ac 100644 --- a/test/unit/org/apache/cassandra/db/TopPartitionTrackerTest.java +++ b/test/unit/org/apache/cassandra/db/TopPartitionTrackerTest.java @@ -21,7 +21,6 @@ package org.apache.cassandra.db; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -36,13 +35,14 @@ import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.metrics.TopPartitionTracker; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; +import static java.util.Collections.singleton; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -56,7 +56,7 @@ public class TopPartitionTrackerTest extends CQLTester DatabaseDescriptor.setMaxTopSizePartitionCount(5); DatabaseDescriptor.setMinTrackedPartitionSizeInBytes(new DataStorageSpec.LongBytesBound("12B")); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); TopPartitionTracker.Collector collector = new TopPartitionTracker.Collector(fullRange); for (int i = 5; i < 15; i++) @@ -87,7 +87,7 @@ public class TopPartitionTrackerTest extends CQLTester createTable("create table %s (id bigint primary key, x int)"); DatabaseDescriptor.setMinTrackedPartitionSizeInBytes(new DataStorageSpec.LongBytesBound("0B")); DatabaseDescriptor.setMaxTopSizePartitionCount(5); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); TopPartitionTracker.Collector collector = new TopPartitionTracker.Collector(fullRange); for (int i = 5; i < 15; i++) @@ -113,7 +113,7 @@ public class TopPartitionTrackerTest extends CQLTester createTable("create table %s (id bigint primary key, x int)"); DatabaseDescriptor.setMinTrackedPartitionSizeInBytes(new DataStorageSpec.LongBytesBound("0B")); DatabaseDescriptor.setMaxTopSizePartitionCount(10); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); TopPartitionTracker.Collector collector = new TopPartitionTracker.Collector(fullRange); for (int i = 0; i < 10; i++) @@ -144,7 +144,7 @@ public class TopPartitionTrackerTest extends CQLTester DatabaseDescriptor.setMinTrackedPartitionTombstoneCount(0); DatabaseDescriptor.setMaxTopSizePartitionCount(10); DatabaseDescriptor.setMaxTopTombstonePartitionCount(10); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); assertEquals(0, tpt.topSizes().lastUpdate); assertEquals(0, tpt.topTombstones().lastUpdate); @@ -214,7 +214,7 @@ public class TopPartitionTrackerTest extends CQLTester for (int i = 0; i < keyCount; i++) keys.add(dk(i)); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); List> expected = new ArrayList<>(); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); TopPartitionTracker.Collector collector = new TopPartitionTracker.Collector(fullRange); @@ -262,7 +262,7 @@ public class TopPartitionTrackerTest extends CQLTester for (int i = 0; i < 10000; i++) keys.add(Pair.create(dk(i), Math.abs(r.nextLong() % 20000))); - Collection> fullRange = Collections.singleton(r(0, 0)); + Collection> fullRange = singleton(r(0, 0)); TopPartitionTracker tpt = new TopPartitionTracker(getCurrentColumnFamilyStore().metadata()); TopPartitionTracker.Collector collector = new TopPartitionTracker.Collector(fullRange); for (int i = 0; i < keys.size(); i++) @@ -271,9 +271,9 @@ public class TopPartitionTrackerTest extends CQLTester collector.trackPartitionSize(entry.left, entry.right); } tpt.merge(collector); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.updateNormalToken(t(0), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(t(Long.MAX_VALUE - 1), InetAddressAndPort.getByName("127.0.0.2")); + InetAddressAndPort ep2 = InetAddressAndPort.getByName("127.0.0.2"); + ClusterMetadataTestHelper.register(ep2); + ClusterMetadataTestHelper.join(ep2, singleton(t(Long.MAX_VALUE - 1))); Iterator trackedTop = tpt.topSizes().top.iterator(); Collection> localRanges = StorageService.instance.getLocalReplicas(keyspace()).ranges(); int outOfRangeCount = 0; diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java index 6fe3d0743b..f23e16788e 100644 --- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java +++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java @@ -48,11 +48,11 @@ public class CommitLogSegmentManagerCDCTest extends CQLTester { private static final Random random = new Random(); + // shadow CQLTester#setUpClass @BeforeClass public static void setUpClass() { ServerTestUtils.daemonInitialization(); - DatabaseDescriptor.setCDCEnabled(true); DatabaseDescriptor.setCDCTotalSpaceInMiB(1024); CQLTester.setUpClass(); diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java index f5712cad2f..92aa8d81ab 100644 --- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java +++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogTest.java @@ -29,14 +29,21 @@ import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.zip.CRC32; import java.util.zip.Checksum; - import javax.crypto.Cipher; import com.google.common.collect.Iterables; @@ -44,30 +51,36 @@ import com.google.common.io.Files; import org.apache.cassandra.io.util.FileOutputStreamPlus; -import org.junit.*; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.memtable.SkipListMemtable; -import org.apache.cassandra.io.compress.ZstdCompressor; -import org.apache.cassandra.io.util.FileUtils; -import org.apache.cassandra.schema.MemtableParams; -import org.apache.cassandra.io.util.RandomAccessReader; -import org.apache.cassandra.schema.SchemaTestUtil; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.config.Config.DiskFailurePolicy; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; -import org.apache.cassandra.config.Config.DiskFailurePolicy; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.MutationExceededMaxSizeException; +import org.apache.cassandra.db.RowUpdateBuilder; import org.apache.cassandra.db.commitlog.CommitLogReplayer.CommitLogReplayException; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.marshal.AsciiType; import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.SetType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.memtable.SkipListMemtable; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.exceptions.ConfigurationException; @@ -75,13 +88,19 @@ import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.compress.DeflateCompressor; import org.apache.cassandra.io.compress.LZ4Compressor; import org.apache.cassandra.io.compress.SnappyCompressor; +import org.apache.cassandra.io.compress.ZstdCompressor; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.MemtableParams; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.security.CipherFactory; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.security.EncryptionContextGenerator; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Hex; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -94,17 +113,11 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.COMMITLOG_ import static org.apache.cassandra.config.CassandraRelevantProperties.COMMIT_LOG_REPLAY_LIST; import static org.apache.cassandra.db.commitlog.CommitLogSegment.ENTRY_OVERHEAD_SIZE; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.apache.cassandra.db.marshal.IntegerType; -import org.apache.cassandra.db.marshal.MapType; -import org.apache.cassandra.db.marshal.SetType; -import org.apache.cassandra.db.marshal.UTF8Type; - @Ignore @RunWith(Parameterized.class) public abstract class CommitLogTest @@ -159,7 +172,7 @@ public abstract class CommitLogTest KeyspaceParams.DEFAULT_LOCAL_DURABLE_WRITES = false; SchemaLoader.prepareServer(); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), FBUtilities.getBroadcastAddressAndPort()); +// StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), FBUtilities.getBroadcastAddressAndPort()); MemtableParams skipListMemtable = MemtableParams.get("skiplist"); diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java index c3a827aae6..42fcadb017 100644 --- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java +++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogUpgradeTest.java @@ -25,31 +25,29 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.Properties; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileInputStreamPlus; -import org.junit.Assert; - import com.google.common.base.Predicate; - import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.db.Mutation; -import org.apache.cassandra.db.rows.Cell; -import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.marshal.AsciiType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileInputStreamPlus; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Tables; import org.apache.cassandra.security.EncryptionContextGenerator; import org.apache.cassandra.utils.JVMStabilityInspector; diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java index f1777cca09..1f15529aa2 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionControllerTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.schema.TableMetadata; @@ -74,7 +75,7 @@ public class CompactionControllerTest extends SchemaLoader @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), TableMetadata.builder(KEYSPACE, CF1) diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java index 2dc4e12df5..076ef9876f 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java @@ -29,9 +29,11 @@ import java.util.regex.Pattern; import com.google.common.collect.*; +import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; @@ -61,27 +63,29 @@ public class CompactionIteratorTest extends CQLTester private static final String KSNAME = "CompactionIteratorTest"; private static final String CFNAME = "Integer1"; - static final DecoratedKey kk; - static final TableMetadata metadata; + private static final DecoratedKey kk; + private static final TableMetadata metadata; private static final int RANGE = 1000; private static final int COUNT = 100; Map, DeletionTime> deletionTimes = new HashMap<>(); - static { - DatabaseDescriptor.daemonInitialization(); - + static + { kk = Util.dk("key"); + metadata = SchemaLoader.standardCFMD(KSNAME, + CFNAME, + 1, + UTF8Type.instance, + Int32Type.instance, + Int32Type.instance).build(); + } - SchemaLoader.prepareServer(); - SchemaLoader.createKeyspace(KSNAME, - KeyspaceParams.simple(1), - metadata = SchemaLoader.standardCFMD(KSNAME, - CFNAME, - 1, - UTF8Type.instance, - Int32Type.instance, - Int32Type.instance).build()); + @BeforeClass + public static void setupClass() + { + SchemaLoader.createKeyspace(KSNAME, KeyspaceParams.simple(1), metadata); + ServerTestUtils.markCMS(); } // See org.apache.cassandra.db.rows.UnfilteredRowsGenerator.parse for the syntax used in these tests. diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerBoundaryReloadTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerBoundaryReloadTest.java index 0d3b0d0e32..deb5b659f7 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerBoundaryReloadTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerBoundaryReloadTest.java @@ -21,14 +21,18 @@ package org.apache.cassandra.db.compaction; import java.net.UnknownHostException; import java.util.List; +import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.DiskBoundaries; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -37,14 +41,28 @@ import static org.junit.Assert.assertTrue; public class CompactionStrategyManagerBoundaryReloadTest extends CQLTester { + + // This method will be run instead of CQLTester#setUpClass + @BeforeClass + public static void setUpClass() + { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + ServerTestUtils.markCMS(); + } + @Test public void testNoReload() { + ClusterMetadataTestHelper.register(FBUtilities.getBroadcastAddressAndPort()); + ClusterMetadataTestHelper.join(FBUtilities.getBroadcastAddressAndPort(), + DatabaseDescriptor.getPartitioner().getRandomToken()); createTable("create table %s (id int primary key)"); ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); List> strategies = cfs.getCompactionStrategyManager().getStrategies(); DiskBoundaries db = cfs.getDiskBoundaries(); - StorageService.instance.getTokenMetadata().invalidateCachedRings(); + cfs.invalidateLocalRanges(); // make sure the strategy instances are the same (no reload) assertTrue(isSame(strategies, cfs.getCompactionStrategyManager().getStrategies())); // but disk boundaries are not .equal (ring version changed) @@ -65,9 +83,11 @@ public class CompactionStrategyManagerBoundaryReloadTest extends CQLTester ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); List> strategies = cfs.getCompactionStrategyManager().getStrategies(); DiskBoundaries db = cfs.getDiskBoundaries(); - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.updateNormalToken(tmd.partitioner.getMinimumToken(), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(tmd.partitioner.getMaximumToken(), InetAddressAndPort.getByName("127.0.0.2")); + ClusterMetadataTestHelper.register(FBUtilities.getBroadcastAddressAndPort()); + ClusterMetadataTestHelper.join(FBUtilities.getBroadcastAddressAndPort(), new Murmur3Partitioner.LongToken(1)); + InetAddressAndPort otherEp = InetAddressAndPort.getByName("127.0.0.2"); + ClusterMetadataTestHelper.register(otherEp); + ClusterMetadataTestHelper.join(otherEp, new Murmur3Partitioner.LongToken(1000)); // make sure the strategy instances have been reloaded assertFalse(isSame(strategies, cfs.getCompactionStrategyManager().getStrategies())); diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java index d66c357c8c..19b758f73e 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionStrategyManagerTest.java @@ -62,6 +62,7 @@ import org.apache.cassandra.notifications.SSTableDeletingNotification; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; @@ -362,7 +363,7 @@ public class CompactionStrategyManagerTest DiskBoundaries boundaries = new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), Lists.newArrayList(forKey(100), forKey(200), forKey(300)), - 10, 10); + Epoch.create(10), 10); CompactionStrategyManager csm = new CompactionStrategyManager(cfs, () -> boundaries, true); @@ -530,7 +531,7 @@ public class CompactionStrategyManagerTest private DiskBoundaries createDiskBoundaries(ColumnFamilyStore cfs, Integer[] boundaries) { List positions = Arrays.stream(boundaries).map(b -> Util.token(String.format(String.format("%04d", b))).minKeyBound()).collect(Collectors.toList()); - return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), positions, 0, 0); + return new DiskBoundaries(cfs, cfs.getDirectories().getWriteableLocations(), positions, Epoch.create(0), 0); } } @@ -555,7 +556,7 @@ public class CompactionStrategyManagerTest { MockCFS(ColumnFamilyStore cfs, Directories dirs) { - super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata, dirs, false, false, true); + super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata.get(), dirs, false, false); } } @@ -566,7 +567,7 @@ public class CompactionStrategyManagerTest private MockCFSForCSM(ColumnFamilyStore cfs, CountDownLatch latch, AtomicInteger upgradeTaskCount) { - super(cfs.keyspace, cfs.name, Util.newSeqGen(10), cfs.metadata, cfs.getDirectories(), true, false, false); + super(cfs.keyspace, cfs.name, Util.newSeqGen(10), cfs.metadata.get(), cfs.getDirectories(), true, false); this.latch = latch; this.upgradeTaskCount = upgradeTaskCount; } diff --git a/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java b/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java index 11592f01d5..fb9c75c6e0 100644 --- a/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/LeveledGenerationsTest.java @@ -47,7 +47,6 @@ public class LeveledGenerationsTest extends CQLTester public static void setUp() { DatabaseDescriptor.daemonInitialization(); - MockSchema.cleanup(); } @Test diff --git a/test/unit/org/apache/cassandra/db/compaction/NeverPurgeTest.java b/test/unit/org/apache/cassandra/db/compaction/NeverPurgeTest.java index 3f5e4b6f5d..e430681c99 100644 --- a/test/unit/org/apache/cassandra/db/compaction/NeverPurgeTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/NeverPurgeTest.java @@ -36,13 +36,17 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import static org.apache.cassandra.config.CassandraRelevantProperties.NEVER_PURGE_TOMBSTONES; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class NeverPurgeTest extends CQLTester { @BeforeClass - public static void setUpClass() // method name must match the @BeforeClass annotated method in CQLTester + // note that the name of this method is important - it shadows the same method CQLTester to + // avoid statically initializing CompactionController before setting this prop + public static void setUpClass() { NEVER_PURGE_TOMBSTONES.setBoolean(true); + assertTrue(CompactionController.NEVER_PURGE_TOMBSTONES_PROPERTY_VALUE); CQLTester.setUpClass(); } diff --git a/test/unit/org/apache/cassandra/db/compaction/PartialCompactionsTest.java b/test/unit/org/apache/cassandra/db/compaction/PartialCompactionsTest.java index 5e0ed66dbf..d9fcccbaa7 100644 --- a/test/unit/org/apache/cassandra/db/compaction/PartialCompactionsTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/PartialCompactionsTest.java @@ -53,11 +53,11 @@ public class PartialCompactionsTest extends SchemaLoader @BeforeClass public static void initSchema() { - CompactionManager.instance.disableAutoCompaction(); - + SchemaLoader.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, TABLE)); + CompactionManager.instance.disableAutoCompaction(); LimitableDataDirectory.applyTo(KEYSPACE, TABLE); } @@ -193,7 +193,7 @@ public class PartialCompactionsTest extends SchemaLoader ColumnFamilyStore store = keyspace.getColumnFamilyStore(cf); TableMetadataRef metadata = store.metadata; keyspace.dropCf(metadata.id, true); - ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(keyspace, cf, metadata, wrapDirectoriesOf(store), false, false, true); + ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(keyspace, cf, metadata.get(), wrapDirectoriesOf(store), false, false); keyspace.initCfCustom(cfs); } diff --git a/test/unit/org/apache/cassandra/db/compaction/ShardManagerTest.java b/test/unit/org/apache/cassandra/db/compaction/ShardManagerTest.java index 9ea03e695b..71e5d8a064 100644 --- a/test/unit/org/apache/cassandra/db/compaction/ShardManagerTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/ShardManagerTest.java @@ -30,6 +30,7 @@ import org.junit.Before; import org.junit.Test; import org.agrona.collections.IntArrayList; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.BufferDecoratedKey; import org.apache.cassandra.db.ColumnFamilyStore; @@ -44,6 +45,7 @@ import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.mockito.Mockito; +import static org.apache.cassandra.db.ColumnFamilyStore.RING_VERSION_IRRELEVANT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -63,7 +65,8 @@ public class ShardManagerTest { DatabaseDescriptor.daemonInitialization(); // because of all the static initialization in CFS DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); - weightedRanges = new ColumnFamilyStore.VersionedLocalRanges(-1, 16); + ServerTestUtils.prepareServerNoRegister(); + weightedRanges = new ColumnFamilyStore.VersionedLocalRanges(RING_VERSION_IRRELEVANT, 16); } @Test @@ -338,7 +341,7 @@ public class ShardManagerTest ColumnFamilyStore.VersionedLocalRanges localRanges(List ranges) { - ColumnFamilyStore.VersionedLocalRanges versionedLocalRanges = new ColumnFamilyStore.VersionedLocalRanges(-1, ranges.size()); + ColumnFamilyStore.VersionedLocalRanges versionedLocalRanges = new ColumnFamilyStore.VersionedLocalRanges(RING_VERSION_IRRELEVANT, ranges.size()); versionedLocalRanges.addAll(ranges); return versionedLocalRanges; } @@ -348,7 +351,7 @@ public class ShardManagerTest List ranges = ImmutableList.of(new Splitter.WeightedRange(1.0, new Range<>(partitioner.getMinimumToken(), partitioner.getMinimumToken()))); - ColumnFamilyStore.VersionedLocalRanges versionedLocalRanges = new ColumnFamilyStore.VersionedLocalRanges(-1, ranges.size()); + ColumnFamilyStore.VersionedLocalRanges versionedLocalRanges = new ColumnFamilyStore.VersionedLocalRanges(RING_VERSION_IRRELEVANT, ranges.size()); versionedLocalRanges.addAll(ranges); return versionedLocalRanges; } @@ -361,7 +364,7 @@ public class ShardManagerTest private static DiskBoundaries makeDiskBoundaries(ColumnFamilyStore cfs, List diskBoundaries) { List diskPositions = diskBoundaries.stream().map(Token::maxKeyBound).collect(Collectors.toList()); - DiskBoundaries db = new DiskBoundaries(cfs, null, diskPositions, -1, -1); + DiskBoundaries db = new DiskBoundaries(cfs, null, diskPositions, RING_VERSION_IRRELEVANT, -1); return db; } diff --git a/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java index 41a4c47578..4576db78f7 100644 --- a/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/TimeWindowCompactionStrategyTest.java @@ -32,6 +32,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.ColumnFamilyStore; @@ -68,10 +69,7 @@ public class TimeWindowCompactionStrategyTest extends SchemaLoader // Disable tombstone histogram rounding for tests CassandraRelevantProperties.STREAMING_HISTOGRAM_ROUND_SECONDS.setInt(1); ALLOW_UNSAFE_AGGRESSIVE_SSTABLE_EXPIRATION.setBoolean(true); - - - SchemaLoader.prepareServer(); - + ServerTestUtils.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1)); diff --git a/test/unit/org/apache/cassandra/db/compaction/UnifiedCompactionStrategyTest.java b/test/unit/org/apache/cassandra/db/compaction/UnifiedCompactionStrategyTest.java index a673b27b7c..7b3d85d0b7 100644 --- a/test/unit/org/apache/cassandra/db/compaction/UnifiedCompactionStrategyTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/UnifiedCompactionStrategyTest.java @@ -36,6 +36,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.BufferDecoratedKey; import org.apache.cassandra.db.ColumnFamilyStore; @@ -53,6 +54,7 @@ import org.apache.cassandra.dht.Splitter; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Overlaps; import org.apache.cassandra.utils.Pair; @@ -109,9 +111,9 @@ public class UnifiedCompactionStrategyTest long seed = System.currentTimeMillis(); random.setSeed(seed); System.out.println("Random seed: " + seed); - DatabaseDescriptor.daemonInitialization(); // because of all the static initialization in CFS DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); } @@ -139,7 +141,7 @@ public class UnifiedCompactionStrategyTest assertNotNull("Splitter is required with multiple compaction shards", splitter); when(cfs.getPartitioner()).thenReturn(partitioner); - localRanges = cfs.fullWeightedRange(0, partitioner); + localRanges = cfs.fullWeightedRange(ClusterMetadata.current().epoch, partitioner); when(cfs.metadata()).thenReturn(metadata); when(cfs.getTableName()).thenReturn(table); diff --git a/test/unit/org/apache/cassandra/db/compaction/unified/ControllerTest.java b/test/unit/org/apache/cassandra/db/compaction/unified/ControllerTest.java index 628a15e133..b463d13117 100644 --- a/test/unit/org/apache/cassandra/db/compaction/unified/ControllerTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/unified/ControllerTest.java @@ -38,6 +38,7 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.compaction.UnifiedCompactionStrategy; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Overlaps; import org.mockito.Mock; @@ -66,7 +67,7 @@ public class ControllerTest UnifiedCompactionStrategy strategy; protected String keyspaceName = "TestKeyspace"; - protected DiskBoundaries diskBoundaries = new DiskBoundaries(cfs, null, null, 0, 0); + protected DiskBoundaries diskBoundaries = new DiskBoundaries(cfs, null, null, Epoch.FIRST, 0); @BeforeClass public static void setUpClass() @@ -545,11 +546,11 @@ public class ControllerTest assertEquals(Controller.DEFAULT_BASE_SHARD_COUNT, controller.baseShardCount); PartitionPosition min = Util.testPartitioner().getMinimumToken().minKeyBound(); - diskBoundaries = new DiskBoundaries(cfs, null, ImmutableList.of(min, min, min), 0, 0); + diskBoundaries = new DiskBoundaries(cfs, null, ImmutableList.of(min, min, min), Epoch.FIRST, 0); controller = Controller.fromOptions(cfs, options); assertEquals(4, controller.baseShardCount); - diskBoundaries = new DiskBoundaries(cfs, null, ImmutableList.of(min), 0, 0); + diskBoundaries = new DiskBoundaries(cfs, null, ImmutableList.of(min), Epoch.FIRST, 0); controller = Controller.fromOptions(cfs, options); assertEquals(Controller.DEFAULT_BASE_SHARD_COUNT, controller.baseShardCount); } diff --git a/test/unit/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriterTest.java b/test/unit/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriterTest.java index 9eb05dc503..2ebe1bdf83 100644 --- a/test/unit/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriterTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/unified/ShardedCompactionWriterTest.java @@ -26,7 +26,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; import org.apache.cassandra.cql3.CQLTester; @@ -45,10 +45,10 @@ import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; +import static org.apache.cassandra.db.ColumnFamilyStore.RING_VERSION_IRRELEVANT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -59,16 +59,12 @@ public class ShardedCompactionWriterTest extends CQLTester private static final int ROW_PER_PARTITION = 10; - @BeforeClass - public static void beforeClass() + @Before + public void before() { - CQLTester.setUpClass(); - CQLTester.prepareServer(); - StorageService.instance.initServer(); - // Disabling durable write since we don't care schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes=false"); - schemaChange(String.format("CREATE TABLE %s.%s (k int, t int, v blob, PRIMARY KEY (k, t))", KEYSPACE, TABLE)); + schemaChange(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int, t int, v blob, PRIMARY KEY (k, t))", KEYSPACE, TABLE)); } @AfterClass @@ -110,7 +106,7 @@ public class ShardedCompactionWriterTest extends CQLTester LifecycleTransaction txn = cfs.getTracker().tryModify(cfs.getLiveSSTables(), OperationType.COMPACTION); - ShardManager boundaries = new ShardManagerNoDisks(ColumnFamilyStore.fullWeightedRange(-1, cfs.getPartitioner())); + ShardManager boundaries = new ShardManagerNoDisks(ColumnFamilyStore.fullWeightedRange(RING_VERSION_IRRELEVANT, cfs.getPartitioner())); ShardedCompactionWriter writer = new ShardedCompactionWriter(cfs, cfs.getDirectories(), txn, txn.originals(), false, boundaries.boundaries(numShards)); int rows = compact(cfs, txn, writer); diff --git a/test/unit/org/apache/cassandra/db/compaction/unified/ShardedMultiWriterTest.java b/test/unit/org/apache/cassandra/db/compaction/unified/ShardedMultiWriterTest.java index 3c1e1181bb..dd87174f20 100644 --- a/test/unit/org/apache/cassandra/db/compaction/unified/ShardedMultiWriterTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/unified/ShardedMultiWriterTest.java @@ -38,7 +38,6 @@ public class ShardedMultiWriterTest extends CQLTester @BeforeClass public static void beforeClass() { - CQLTester.setUpClass(); StorageService.instance.initServer(); } diff --git a/test/unit/org/apache/cassandra/db/compaction/writers/CompactionAwareWriterTest.java b/test/unit/org/apache/cassandra/db/compaction/writers/CompactionAwareWriterTest.java index 25d8db5208..b9142db88a 100644 --- a/test/unit/org/apache/cassandra/db/compaction/writers/CompactionAwareWriterTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/writers/CompactionAwareWriterTest.java @@ -35,6 +35,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.QueryProcessor; @@ -63,11 +64,14 @@ public class CompactionAwareWriterTest extends CQLTester private static final int ROW_PER_PARTITION = 10; @BeforeClass - public static void beforeClass() throws Throwable + public static void setUpClass() { + // Don't register/join the local node so that DiskBoundaries are empty (testMultiDatadirCheck depends on this) + ServerTestUtils.prepareServerNoRegister(); // Disabling durable write since we don't care schemaChange("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes=false"); schemaChange(String.format("CREATE TABLE %s.%s (k int, t int, v blob, PRIMARY KEY (k, t))", KEYSPACE, TABLE)); + ServerTestUtils.markCMS(); } @AfterClass diff --git a/test/unit/org/apache/cassandra/db/context/CounterContextTest.java b/test/unit/org/apache/cassandra/db/context/CounterContextTest.java index 5eda484744..d0070dd3ab 100644 --- a/test/unit/org/apache/cassandra/db/context/CounterContextTest.java +++ b/test/unit/org/apache/cassandra/db/context/CounterContextTest.java @@ -31,8 +31,12 @@ import org.apache.cassandra.db.ClockAndCount; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.context.CounterContext.Relationship; import org.apache.cassandra.db.marshal.ByteBufferAccessor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.CounterId; +import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -58,6 +62,9 @@ public class CounterContextTest { DatabaseDescriptor.daemonInitialization(); CommitLog.instance.start(); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); + ClusterMetadataTestHelper.register(FBUtilities.getBroadcastAddressAndPort()); } @Test diff --git a/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java b/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java index 3cf7c59e9f..32fa3b234a 100644 --- a/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java +++ b/test/unit/org/apache/cassandra/db/filter/ColumnFilterTest.java @@ -20,13 +20,17 @@ package org.apache.cassandra.db.filter; import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.function.Consumer; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; +import org.junit.runners.Parameterized; +import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; @@ -36,7 +40,6 @@ import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; @@ -49,6 +52,7 @@ import org.apache.cassandra.utils.Throwables; import static org.junit.Assert.assertEquals; +@Ignore("TCM - rewrite test, nothing in this test is now depending on version") public class ColumnFilterTest { private static final ColumnFilter.Serializer serializer = new ColumnFilter.Serializer(); @@ -75,22 +79,30 @@ public class ColumnFilterTest private final CellPath path3 = CellPath.create(ByteBufferUtil.bytes(3)); private final CellPath path4 = CellPath.create(ByteBufferUtil.bytes(4)); + @Parameterized.Parameters(name = "{index}: clusterMinVersion={0}") + public static Collection data() + { + // [tcm] we will require upgrading from 4.1 + return Arrays.asList(new Object[]{ "4.1" }, new Object[]{ "4.0" }); + } + @BeforeClass public static void beforeClass() { // Gossiper touches StorageService which touches StreamManager which requires configs be setup DatabaseDescriptor.daemonInitialization(); + SchemaLoader.prepareServer(); DatabaseDescriptor.setSeedProvider(Arrays::asList); DatabaseDescriptor.setEndpointSnitch(new SimpleSnitch()); DatabaseDescriptor.setDefaultFailureDetector(); DatabaseDescriptor.setPartitionerUnsafe(new Murmur3Partitioner()); - Gossiper.instance.start(0); } @Before public void before() { Util.setUpgradeFromVersion("4.0"); + // todo; nothing in this test is actually version dependent anymore } // Select all diff --git a/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java b/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java index 8952262e2b..6cd08d0001 100644 --- a/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java +++ b/test/unit/org/apache/cassandra/db/filter/RowFilterTest.java @@ -58,6 +58,7 @@ public class RowFilterTest .addPartitionKeyColumn("pk", Int32Type.instance) .addStaticColumn("s", Int32Type.instance) .addRegularColumn("r", Int32Type.instance) + .offline() .build(); ColumnMetadata s = metadata.getColumn(new ColumnIdentifier("s", true)); ColumnMetadata r = metadata.getColumn(new ColumnIdentifier("r", true)); diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailKeyspacesTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailKeyspacesTest.java index b0843d4b99..d6238264d1 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailKeyspacesTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailKeyspacesTest.java @@ -19,14 +19,11 @@ package org.apache.cassandra.db.guardrails; -import java.util.concurrent.TimeUnit; - import org.junit.After; import org.junit.Test; import org.apache.cassandra.config.Converters; import org.apache.cassandra.schema.Schema; -import org.awaitility.Awaitility; import static java.lang.String.format; @@ -59,13 +56,6 @@ public class GuardrailKeyspacesTest extends ThresholdTester { // CQLTester deletes keyspaces after tests, but does so asynchronously super.afterTest(); - - // Wait until only cql_test_keyspace remains - Awaitility.await() - .atMost(10, TimeUnit.MINUTES) - .pollDelay(0, TimeUnit.MILLISECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> Schema.instance.getUserKeyspaces().size() == 1); } @Test diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumReplicationFactorTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumReplicationFactorTest.java index 865ac23c79..04a7e33b3e 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumReplicationFactorTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumReplicationFactorTest.java @@ -21,7 +21,6 @@ package org.apache.cassandra.db.guardrails; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import org.junit.After; @@ -30,12 +29,12 @@ import org.junit.Test; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.AbstractEndpointSnitch; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.StorageService; import org.assertj.core.api.Assertions; import static java.lang.String.format; @@ -132,7 +131,8 @@ public class GuardrailMaximumReplicationFactorTest extends ThresholdTester List twoWarnings = Arrays.asList(format("The keyspace ks has a replication factor of 3, above the warning threshold of %s.", MAXIMUM_REPLICATION_FACTOR_WARN_THRESHOLD), format("The keyspace ks has a replication factor of 3, above the warning threshold of %s.", MAXIMUM_REPLICATION_FACTOR_WARN_THRESHOLD)); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.255")); + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.0.0.255"), "datacenter2", RACK1); + guardrails().setMaximumReplicationFactorThreshold(MAXIMUM_REPLICATION_FACTOR_WARN_THRESHOLD, MAXIMUM_REPLICATION_FACTOR_FAIL_THRESHOLD); assertValid("CREATE KEYSPACE ks WITH replication = { 'class' : 'NetworkTopologyStrategy', 'datacenter1': 2, 'datacenter2' : 2}"); execute("DROP KEYSPACE IF EXISTS ks"); diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumReplicationFactorTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumReplicationFactorTest.java index 8817f9a8c6..173f70698b 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumReplicationFactorTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumReplicationFactorTest.java @@ -21,7 +21,6 @@ package org.apache.cassandra.db.guardrails; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import org.junit.After; @@ -36,7 +35,8 @@ import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.transformations.Register; import org.assertj.core.api.Assertions; import static java.lang.String.format; @@ -171,7 +171,8 @@ public class GuardrailMinimumReplicationFactorTest extends ThresholdTester List twoWarnings = Arrays.asList(format("The keyspace %s has a replication factor of 2, below the warning threshold of %d.", KS, MINIMUM_REPLICATION_FACTOR_WARN_THRESHOLD), format("The keyspace %s has a replication factor of 2, below the warning threshold of %d.", KS, MINIMUM_REPLICATION_FACTOR_WARN_THRESHOLD)); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.255")); + InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0.255"); + Register.register(new NodeAddresses(ep)); guardrails().setMinimumReplicationFactorThreshold(MINIMUM_REPLICATION_FACTOR_WARN_THRESHOLD, MINIMUM_REPLICATION_FACTOR_FAIL_THRESHOLD); assertValid("CREATE KEYSPACE ks WITH replication = { 'class' : 'NetworkTopologyStrategy', 'datacenter1': 4, 'datacenter2' : 4 }"); execute("DROP KEYSPACE IF EXISTS ks"); diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesTest.java index 352af8f2db..24dddbb48f 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesTest.java @@ -61,8 +61,7 @@ public class GuardrailTablesTest extends ThresholdTester { // Convert and set a deprecated threshold value based on the total number of tables, not just user tables int convertedValue = (int) Converters.TABLE_COUNT_THRESHOLD_TO_GUARDRAIL.convert(Schema.instance.getNumberOfTables()); - Guardrails.instance.setTablesThreshold(convertedValue + 1, TABLES_LIMIT_FAIL_THRESHOLD); - + Guardrails.instance.setTablesThreshold(convertedValue, TABLES_LIMIT_FAIL_THRESHOLD); assertCreateTable(); } diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java index f2744abf57..2a13cfb23f 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java @@ -51,6 +51,7 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.guardrails.GuardrailEvent.GuardrailEventType; import org.apache.cassandra.db.view.View; import org.apache.cassandra.diag.DiagnosticEventService; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.sasi.SASIIndex; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; @@ -110,13 +111,11 @@ public abstract class GuardrailTester extends CQLTester } @BeforeClass - public static void setUpClass() + public static void setUpState() { - CQLTester.setUpClass(); requireAuthentication(); requireNetwork(); DatabaseDescriptor.setDiagnosticEventsEnabled(true); - systemClientState = ClientState.forInternalCalls(); userClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 123)); @@ -218,7 +217,7 @@ public abstract class GuardrailTester extends CQLTester listener.assertNotWarned(); listener.assertNotFailed(); } - catch (GuardrailViolatedException e) + catch (InvalidRequestException e) { fail("Expected not to fail, but failed with error message: " + e.getMessage()); } @@ -352,7 +351,7 @@ public abstract class GuardrailTester extends CQLTester if (thrown) fail("Expected to fail, but it did not"); } - catch (GuardrailViolatedException e) + catch (InvalidRequestException e) // TODO: this used to catch GuardrailViolatedException, but now we throw InvalidRequestException for all rejections in Schema#submit { assertTrue("Expect no exception thrown", thrown); diff --git a/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java b/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java index 20af2a0c18..6f72c21334 100644 --- a/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java +++ b/test/unit/org/apache/cassandra/db/lifecycle/HelpersTest.java @@ -31,7 +31,8 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.Assert; -import org.apache.cassandra.config.DatabaseDescriptor; + +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.OperationType; @@ -48,7 +49,7 @@ public class HelpersTest @BeforeClass public static void setUp() { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); MockSchema.cleanup(); } diff --git a/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java b/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java index 781a46884d..8f197c1072 100644 --- a/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java +++ b/test/unit/org/apache/cassandra/db/lifecycle/LifecycleTransactionTest.java @@ -23,17 +23,17 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.junit.Assert; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.commitlog.CommitLogPosition; import org.apache.cassandra.db.compaction.OperationType; -import org.apache.cassandra.db.lifecycle.LifecycleTransaction.ReaderState.Action; import org.apache.cassandra.db.lifecycle.LifecycleTransaction.ReaderState; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction.ReaderState.Action; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.MockSchema; import org.apache.cassandra.utils.Pair; diff --git a/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java b/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java index 28aab8556e..b29dadc60f 100644 --- a/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java +++ b/test/unit/org/apache/cassandra/db/lifecycle/RealTransactionsTest.java @@ -25,6 +25,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import org.junit.Assert; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.io.util.File; import org.junit.BeforeClass; import org.junit.Test; @@ -41,7 +43,6 @@ import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableRewriter; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadataRef; @@ -65,7 +66,7 @@ public class RealTransactionsTest extends SchemaLoader @BeforeClass public static void setUp() { - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, REWRITE_FINISHED_CF), diff --git a/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java b/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java index 7192d50031..c082ad40bb 100644 --- a/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java +++ b/test/unit/org/apache/cassandra/db/lifecycle/TrackerTest.java @@ -34,6 +34,7 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.commitlog.CommitLog; @@ -86,7 +87,7 @@ public class TrackerTest @BeforeClass public static void setUp() { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); MockSchema.cleanup(); } diff --git a/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java b/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java index eb162d59b9..4dbb93ead1 100644 --- a/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java +++ b/test/unit/org/apache/cassandra/db/lifecycle/ViewTest.java @@ -32,7 +32,8 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.Assert; -import org.apache.cassandra.config.DatabaseDescriptor; + +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.commitlog.CommitLog; @@ -52,7 +53,7 @@ public class ViewTest @BeforeClass public static void setUp() { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); MockSchema.cleanup(); } diff --git a/test/unit/org/apache/cassandra/db/memtable/MemtableQuickTest.java b/test/unit/org/apache/cassandra/db/memtable/MemtableQuickTest.java index c442a4c036..e07dd9afe6 100644 --- a/test/unit/org/apache/cassandra/db/memtable/MemtableQuickTest.java +++ b/test/unit/org/apache/cassandra/db/memtable/MemtableQuickTest.java @@ -69,8 +69,8 @@ public class MemtableQuickTest extends CQLTester @BeforeClass public static void setUp() { - CQLTester.setUpClass(); - CQLTester.prepareServer(); +// CQLTester.setUpClass(); +// CQLTester.prepareServer(); CQLTester.disablePreparedReuseForTest(); logger.info("setupClass done."); } diff --git a/test/unit/org/apache/cassandra/db/memtable/MemtableSizeTestBase.java b/test/unit/org/apache/cassandra/db/memtable/MemtableSizeTestBase.java index 33d1fcb7e5..25fd8ff190 100644 --- a/test/unit/org/apache/cassandra/db/memtable/MemtableSizeTestBase.java +++ b/test/unit/org/apache/cassandra/db/memtable/MemtableSizeTestBase.java @@ -96,7 +96,6 @@ public abstract class MemtableSizeTestBase extends CQLTester } CQLTester.setUpClass(); - CQLTester.prepareServer(); logger.info("setupClass done, allocation type {}", allocationType); } diff --git a/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java b/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java index 9773454e78..8853688b7e 100644 --- a/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java +++ b/test/unit/org/apache/cassandra/db/partitions/PurgeFunctionTest.java @@ -70,6 +70,7 @@ public final class PurgeFunctionTest @Before public void setUp() { + DatabaseDescriptor.clientInitialization(); DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); metadata = diff --git a/test/unit/org/apache/cassandra/db/repair/AbstractPendingAntiCompactionTest.java b/test/unit/org/apache/cassandra/db/repair/AbstractPendingAntiCompactionTest.java index c96b85ff77..7a6b2bd2e7 100644 --- a/test/unit/org/apache/cassandra/db/repair/AbstractPendingAntiCompactionTest.java +++ b/test/unit/org/apache/cassandra/db/repair/AbstractPendingAntiCompactionTest.java @@ -30,18 +30,15 @@ import org.junit.Ignore; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; -import org.apache.cassandra.cql3.statements.schema.IndexTarget; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.repair.AbstractRepairTest; import org.apache.cassandra.repair.consistent.LocalSessionAccessor; -import org.apache.cassandra.schema.IndexMetadata; -import org.apache.cassandra.schema.Indexes; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; @@ -85,17 +82,12 @@ public abstract class AbstractPendingAntiCompactionTest ks = "ks_" + System.currentTimeMillis(); cfm = CreateTableStatement.parse(String.format("CREATE TABLE %s.%s (k INT PRIMARY KEY, v INT)", ks, tbl), ks).build(); - Indexes.Builder indexes = Indexes.builder(); - indexes.add(IndexMetadata.fromIndexTargets(Collections.singletonList(new IndexTarget(new ColumnIdentifier("v", true), - IndexTarget.Type.VALUES)), - tbl2 + "_idx", - IndexMetadata.Kind.COMPOSITES, Collections.emptyMap())); - - TableMetadata cfm2 = CreateTableStatement.parse(String.format("CREATE TABLE %s.%s (k INT PRIMARY KEY, v INT)", ks, tbl2), ks).indexes(indexes.build()).build(); + TableMetadata cfm2 = CreateTableStatement.parse(String.format("CREATE TABLE %s.%s (k INT PRIMARY KEY, v INT)", ks, tbl2), ks).build(); SchemaLoader.createKeyspace(ks, KeyspaceParams.simple(1), cfm, cfm2); cfs = Schema.instance.getColumnFamilyStoreInstance(cfm.id); cfs2 = Schema.instance.getColumnFamilyStoreInstance(cfm2.id); + QueryProcessor.execute(String.format("create index %s_idx on %s.%s (v)", tbl2, ks, tbl2), ConsistencyLevel.ONE); } void makeSSTables(int num) diff --git a/test/unit/org/apache/cassandra/db/rows/PartitionSerializationExceptionTest.java b/test/unit/org/apache/cassandra/db/rows/PartitionSerializationExceptionTest.java index 6c82c7e55d..399cd577ee 100644 --- a/test/unit/org/apache/cassandra/db/rows/PartitionSerializationExceptionTest.java +++ b/test/unit/org/apache/cassandra/db/rows/PartitionSerializationExceptionTest.java @@ -34,7 +34,7 @@ public class PartitionSerializationExceptionTest @Test public void testMessageWithSimplePartitionKey() { - TableMetadata metadata = TableMetadata.builder("ks", "tbl").addPartitionKeyColumn("pk", UTF8Type.instance).build(); + TableMetadata metadata = TableMetadata.builder("ks", "tbl").addPartitionKeyColumn("pk", UTF8Type.instance).offline().build(); DecoratedKey key = mock(DecoratedKey.class); when(key.getKey()).thenReturn(UTF8Type.instance.decompose("foo")); @@ -53,7 +53,8 @@ public class PartitionSerializationExceptionTest { TableMetadata metadata = TableMetadata.builder("ks", "tbl") .addPartitionKeyColumn("pk1", UTF8Type.instance) - .addPartitionKeyColumn("pk2", UTF8Type.instance).build(); + .addPartitionKeyColumn("pk2", UTF8Type.instance) + .offline().build(); DecoratedKey key = mock(DecoratedKey.class); CompositeType keyType = CompositeType.getInstance(UTF8Type.instance, UTF8Type.instance); diff --git a/test/unit/org/apache/cassandra/db/rows/RowsMergingTest.java b/test/unit/org/apache/cassandra/db/rows/RowsMergingTest.java index 01e335d8e9..a4c1f275fd 100644 --- a/test/unit/org/apache/cassandra/db/rows/RowsMergingTest.java +++ b/test/unit/org/apache/cassandra/db/rows/RowsMergingTest.java @@ -31,10 +31,9 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.BTREE_BRAN public class RowsMergingTest extends CQLTester { @BeforeClass - public static void setUpClass() + public static void setSystemProps() { BTREE_BRANCH_SHIFT.setInt(2); - CQLTester.setUpClass(); } @Test diff --git a/test/unit/org/apache/cassandra/db/rows/ThrottledUnfilteredIteratorTest.java b/test/unit/org/apache/cassandra/db/rows/ThrottledUnfilteredIteratorTest.java index d2a9aa7824..4a115b8139 100644 --- a/test/unit/org/apache/cassandra/db/rows/ThrottledUnfilteredIteratorTest.java +++ b/test/unit/org/apache/cassandra/db/rows/ThrottledUnfilteredIteratorTest.java @@ -31,7 +31,6 @@ import java.util.SortedMap; import java.util.TreeMap; import com.google.common.collect.Iterators; -import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; @@ -56,7 +55,6 @@ import org.apache.cassandra.db.partitions.ImmutableBTreePartition; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.schema.ColumnMetadata; @@ -71,15 +69,6 @@ public class ThrottledUnfilteredIteratorTest extends CQLTester private static final String KSNAME = "ThrottledUnfilteredIteratorTest"; private static final String CFNAME = "StandardInteger1"; - @BeforeClass - public static void defineSchema() throws ConfigurationException - { - SchemaLoader.prepareServer(); - SchemaLoader.createKeyspace(KSNAME, - KeyspaceParams.simple(1), - standardCFMD(KSNAME, CFNAME, 1, UTF8Type.instance, Int32Type.instance, Int32Type.instance)); - } - static final TableMetadata metadata; static final ColumnMetadata v1Metadata; static final ColumnMetadata v2Metadata; @@ -606,6 +595,9 @@ public class ThrottledUnfilteredIteratorTest extends CQLTester @Test public void testThrottledIteratorWithRangeDeletions() throws Exception { + SchemaLoader.createKeyspace(KSNAME, + KeyspaceParams.simple(1), + standardCFMD(KSNAME, CFNAME, 1, UTF8Type.instance, Int32Type.instance, Int32Type.instance)); Keyspace keyspace = Keyspace.open(KSNAME); ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CFNAME); diff --git a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java index 9a40823db9..0a4ad2b64e 100644 --- a/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java +++ b/test/unit/org/apache/cassandra/db/rows/UnfilteredRowIteratorsTest.java @@ -51,6 +51,7 @@ public class UnfilteredRowIteratorsTest .addClusteringColumn("ck", Int32Type.instance) .addRegularColumn("v1", Int32Type.instance) .addRegularColumn("v2", Int32Type.instance) + .offline() .build(); v1Metadata = metadata.regularAndStaticColumns().columns(false).getSimple(0); v2Metadata = metadata.regularAndStaticColumns().columns(false).getSimple(1); diff --git a/test/unit/org/apache/cassandra/db/streaming/StreamRequestTest.java b/test/unit/org/apache/cassandra/db/streaming/StreamRequestTest.java index 9f5e656850..18c9169c41 100644 --- a/test/unit/org/apache/cassandra/db/streaming/StreamRequestTest.java +++ b/test/unit/org/apache/cassandra/db/streaming/StreamRequestTest.java @@ -38,6 +38,8 @@ import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.streaming.StreamRequest; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; public class StreamRequestTest { @@ -49,6 +51,7 @@ public class StreamRequestTest public static void setUp() throws Throwable { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); local = InetAddressAndPort.getByName("127.0.0.1"); } diff --git a/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java b/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java index ec0ca28dc8..2820f38915 100644 --- a/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java +++ b/test/unit/org/apache/cassandra/db/transform/RTTransformationsTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import com.google.common.collect.Iterators; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.*; import org.apache.cassandra.db.ClusteringPrefix.Kind; import org.apache.cassandra.db.marshal.AbstractType; @@ -58,6 +59,7 @@ public final class RTTransformationsTest @Before public void setUp() { + DatabaseDescriptor.daemonInitialization(); metadata = TableMetadata.builder(KEYSPACE, TABLE) .addPartitionKeyColumn("pk", UTF8Type.instance) diff --git a/test/unit/org/apache/cassandra/db/view/ViewBuilderTaskTest.java b/test/unit/org/apache/cassandra/db/view/ViewBuilderTaskTest.java index c6df89860c..630cb31aa3 100644 --- a/test/unit/org/apache/cassandra/db/view/ViewBuilderTaskTest.java +++ b/test/unit/org/apache/cassandra/db/view/ViewBuilderTaskTest.java @@ -60,7 +60,7 @@ public class ViewBuilderTaskTest extends CQLTester "PRIMARY KEY (v, k, c)", viewName)); ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); - View view = cfs.keyspace.viewManager.forTable(cfs.metadata().id).iterator().next(); + View view = cfs.keyspace.viewManager.forTable(cfs.metadata()).iterator().next(); // Insert the dataset for (int k = 0; k < 100; k++) diff --git a/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java b/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java index e0ef3d6887..18463c7122 100644 --- a/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java +++ b/test/unit/org/apache/cassandra/db/view/ViewUtilsTest.java @@ -23,26 +23,25 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.*; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.OrderPreservingPartitioner; import org.apache.cassandra.dht.OrderPreservingPartitioner.StringToken; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.NetworkTopologyStrategy; -import org.apache.cassandra.locator.PropertyFileSnitch; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.schema.SchemaTestUtil; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.StubClusterMetadataService; public class ViewUtilsTest { @@ -52,25 +51,30 @@ public class ViewUtilsTest public static void setUp() throws ConfigurationException, IOException { DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(OrderPreservingPartitioner.instance); ServerTestUtils.cleanupAndLeaveDirs(); - IEndpointSnitch snitch = new PropertyFileSnitch(); - DatabaseDescriptor.setEndpointSnitch(snitch); Keyspace.setInitialized(); + + } + + @Before + public void beforeEach() + { + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); } @Test public void testGetIndexNaturalEndpoint() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("A"), "DC1", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("C"), "DC1", "RACK1"); // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("B"), "DC2", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("D"), "DC2", "RACK1"); + Map replicationMap = new HashMap<>(); replicationMap.put(ReplicationParams.CLASS, NetworkTopologyStrategy.class.getName()); @@ -80,7 +84,8 @@ public class ViewUtilsTest recreateKeyspace(replicationMap); - Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(Keyspace.open(KS).getReplicationStrategy(), + Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(ClusterMetadata.current(), + KS, new StringToken("CA"), new StringToken("BB")); @@ -92,16 +97,13 @@ public class ViewUtilsTest @Test public void testLocalHostPreference() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("A"), "DC1", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("C"), "DC1", "RACK1"); // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("B"), "DC2", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("D"), "DC2", "RACK1"); Map replicationMap = new HashMap<>(); replicationMap.put(ReplicationParams.CLASS, NetworkTopologyStrategy.class.getName()); @@ -111,7 +113,8 @@ public class ViewUtilsTest recreateKeyspace(replicationMap); - Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(Keyspace.open(KS).getReplicationStrategy(), + Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(ClusterMetadata.current(), + KS, new StringToken("CA"), new StringToken("BB")); @@ -122,16 +125,14 @@ public class ViewUtilsTest @Test public void testBaseTokenDoesNotBelongToLocalReplicaShouldReturnEmpty() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("A"), "DC1", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("C"), "DC1", "RACK1"); // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("B"), "DC2", "RACK1"); + ClusterMetadataTestHelper.addEndpoint(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("D"), "DC2", "RACK1"); + Map replicationMap = new HashMap<>(); replicationMap.put(ReplicationParams.CLASS, NetworkTopologyStrategy.class.getName()); @@ -141,7 +142,8 @@ public class ViewUtilsTest recreateKeyspace(replicationMap); - Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(Keyspace.open(KS).getReplicationStrategy(), + Optional naturalEndpoint = ViewUtils.getViewNaturalEndpoint(ClusterMetadata.current(), + KS, new StringToken("AB"), new StringToken("BB")); diff --git a/test/unit/org/apache/cassandra/db/virtual/BatchMetricsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/BatchMetricsTableTest.java index 8c3475941c..ed187324bf 100644 --- a/test/unit/org/apache/cassandra/db/virtual/BatchMetricsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/BatchMetricsTableTest.java @@ -22,7 +22,6 @@ import java.util.concurrent.atomic.AtomicInteger; import com.google.common.collect.ImmutableList; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import com.codahale.metrics.Histogram; @@ -39,12 +38,6 @@ public class BatchMetricsTableTest extends CQLTester { private static final String KS_NAME = "vts"; - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - @Before public void config() { diff --git a/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java index 889d862135..b357c00e0f 100644 --- a/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java @@ -71,8 +71,6 @@ public class CIDRFilteringMetricsTableTest extends CQLTester @BeforeClass public static void defineSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); - SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(), new AuthTestUtils.LocalPasswordAuthenticator(), new AuthTestUtils.LocalCassandraAuthorizer(), diff --git a/test/unit/org/apache/cassandra/db/virtual/CQLMetricsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/CQLMetricsTableTest.java index 658b84123d..a048cf6222 100644 --- a/test/unit/org/apache/cassandra/db/virtual/CQLMetricsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/CQLMetricsTableTest.java @@ -29,7 +29,6 @@ import com.datastax.driver.core.Session; import static org.junit.Assert.assertEquals; import org.junit.Test; -import org.junit.BeforeClass; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.QueryProcessor; @@ -39,12 +38,6 @@ public class CQLMetricsTableTest extends CQLTester { private static final String KS_NAME = "vts"; - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - private void queryAndValidateMetrics(CQLMetrics expectedMetrics) throws Throwable { String getMetricsQuery = "SELECT * FROM " + KS_NAME + "." + CQLMetricsTable.TABLE_NAME; diff --git a/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java index 5b9aa14bc1..862a282a23 100644 --- a/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java @@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableList; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import com.datastax.driver.core.ResultSet; @@ -39,12 +38,6 @@ public class ClientsTableTest extends CQLTester private ClientsTable table; - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - @Before public void config() { diff --git a/test/unit/org/apache/cassandra/db/virtual/CredentialsCacheKeysTableTest.java b/test/unit/org/apache/cassandra/db/virtual/CredentialsCacheKeysTableTest.java index 7aa1ec4fc6..9126168461 100644 --- a/test/unit/org/apache/cassandra/db/virtual/CredentialsCacheKeysTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/CredentialsCacheKeysTableTest.java @@ -47,14 +47,12 @@ public class CredentialsCacheKeysTableTest extends CQLTester private CredentialsCacheKeysTable table; @BeforeClass - public static void setUpClass() + public static void setUpAuth() { ServerTestUtils.daemonInitialization(); // high value is used for convenient debugging DatabaseDescriptor.setCredentialsValidity(20_000); - - CQLTester.setUpClass(); CQLTester.requireAuthentication(); passwordAuthenticator = (AuthTestUtils.LocalPasswordAuthenticator) DatabaseDescriptor.getAuthenticator(); diff --git a/test/unit/org/apache/cassandra/db/virtual/GossipInfoTableTest.java b/test/unit/org/apache/cassandra/db/virtual/GossipInfoTableTest.java index 3863cbc189..bd8766e557 100644 --- a/test/unit/org/apache/cassandra/db/virtual/GossipInfoTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/GossipInfoTableTest.java @@ -24,7 +24,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Supplier; import org.awaitility.Awaitility; -import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.cql3.CQLTester; @@ -39,12 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat; public class GossipInfoTableTest extends CQLTester { - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - @Test public void testSelectAllWhenGossipInfoIsEmpty() throws Throwable { diff --git a/test/unit/org/apache/cassandra/db/virtual/JmxPermissionsCacheKeysTableTest.java b/test/unit/org/apache/cassandra/db/virtual/JmxPermissionsCacheKeysTableTest.java index 1dfe622d49..782d282570 100644 --- a/test/unit/org/apache/cassandra/db/virtual/JmxPermissionsCacheKeysTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/JmxPermissionsCacheKeysTableTest.java @@ -55,11 +55,10 @@ public class JmxPermissionsCacheKeysTableTest extends CQLTester // this method is intentionally not called "setUpClass" to let it throw exception brought by startJMXServer method @BeforeClass - public static void setup() throws Exception { + public static void setupAuth() throws Exception { // high value is used for convenient debugging DatabaseDescriptor.setPermissionsValidity(20_000); - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); diff --git a/test/unit/org/apache/cassandra/db/virtual/LocalRepairTablesTest.java b/test/unit/org/apache/cassandra/db/virtual/LocalRepairTablesTest.java index aec6c1574e..e706923fbd 100644 --- a/test/unit/org/apache/cassandra/db/virtual/LocalRepairTablesTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/LocalRepairTablesTest.java @@ -68,8 +68,6 @@ public class LocalRepairTablesTest extends CQLTester @BeforeClass public static void before() { - CQLTester.setUpClass(); - VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, LocalRepairTables.getAll(KS_NAME))); } diff --git a/test/unit/org/apache/cassandra/db/virtual/LogMessagesTableTest.java b/test/unit/org/apache/cassandra/db/virtual/LogMessagesTableTest.java index f3655a4f40..dd32058533 100644 --- a/test/unit/org/apache/cassandra/db/virtual/LogMessagesTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/LogMessagesTableTest.java @@ -25,7 +25,6 @@ import java.util.LinkedList; import java.util.List; import com.google.common.collect.ImmutableList; -import org.junit.BeforeClass; import org.junit.Test; import ch.qos.logback.classic.Level; @@ -47,12 +46,6 @@ public class LogMessagesTableTest extends CQLTester private String keyspace = createKeyspaceName(); private LogMessagesTable table; - @BeforeClass - public static void setup() - { - CQLTester.setUpClass(); - } - @Test public void testTruncate() throws Throwable { diff --git a/test/unit/org/apache/cassandra/db/virtual/NetworkPermissionsCacheKeysTableTest.java b/test/unit/org/apache/cassandra/db/virtual/NetworkPermissionsCacheKeysTableTest.java index e935bbb214..b3ebdea2f5 100644 --- a/test/unit/org/apache/cassandra/db/virtual/NetworkPermissionsCacheKeysTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/NetworkPermissionsCacheKeysTableTest.java @@ -45,14 +45,13 @@ public class NetworkPermissionsCacheKeysTableTest extends CQLTester private NetworkPermissionsCacheKeysTable table; @BeforeClass - public static void setUpClass() + public static void setUpAuth() { ServerTestUtils.daemonInitialization(); // high value is used for convenient debugging DatabaseDescriptor.setPermissionsValidity(20_000); - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); diff --git a/test/unit/org/apache/cassandra/db/virtual/PermissionsCacheKeysTableTest.java b/test/unit/org/apache/cassandra/db/virtual/PermissionsCacheKeysTableTest.java index 429517521f..eb684bddf3 100644 --- a/test/unit/org/apache/cassandra/db/virtual/PermissionsCacheKeysTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/PermissionsCacheKeysTableTest.java @@ -51,13 +51,12 @@ public class PermissionsCacheKeysTableTest extends CQLTester private PermissionsCacheKeysTable table; @BeforeClass - public static void setUpClass() + public static void setUpAuth() { ServerTestUtils.daemonInitialization(); // high value is used for convenient debugging DatabaseDescriptor.setPermissionsValidity(20_000); - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); diff --git a/test/unit/org/apache/cassandra/db/virtual/RolesCacheKeysTableTest.java b/test/unit/org/apache/cassandra/db/virtual/RolesCacheKeysTableTest.java index de14aefd63..7c809faa50 100644 --- a/test/unit/org/apache/cassandra/db/virtual/RolesCacheKeysTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/RolesCacheKeysTableTest.java @@ -45,13 +45,12 @@ public class RolesCacheKeysTableTest extends CQLTester private RolesCacheKeysTable table; @BeforeClass - public static void setUpClass() + public static void setUpAuth() { ServerTestUtils.daemonInitialization(); // high value is used for convenient debugging DatabaseDescriptor.setRolesValidity(20_000); - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); diff --git a/test/unit/org/apache/cassandra/db/virtual/SSTableTasksTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SSTableTasksTableTest.java index 6e8a13612b..e6de58d37a 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SSTableTasksTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SSTableTasksTableTest.java @@ -47,9 +47,8 @@ public class SSTableTasksTableTest extends CQLTester private SSTableTasksTable table; @BeforeClass - public static void setUpClass() + public static void setUpAutoCompaction() { - CQLTester.setUpClass(); CompactionManager.instance.disableAutoCompaction(); } diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java index a5c69cd240..d9aabcb054 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java @@ -25,7 +25,6 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import com.datastax.driver.core.ResultSet; @@ -45,12 +44,6 @@ public class SettingsTableTest extends CQLTester private Config config; private SettingsTable table; - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - @Before public void config() { diff --git a/test/unit/org/apache/cassandra/db/virtual/StreamingVirtualTableTest.java b/test/unit/org/apache/cassandra/db/virtual/StreamingVirtualTableTest.java index 56d01935ed..07f2815c86 100644 --- a/test/unit/org/apache/cassandra/db/virtual/StreamingVirtualTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/StreamingVirtualTableTest.java @@ -67,7 +67,6 @@ public class StreamingVirtualTableTest extends CQLTester @BeforeClass public static void setup() { - CQLTester.setUpClass(); StreamingVirtualTable table = new StreamingVirtualTable(KS_NAME); TABLE_NAME = table.toString(); VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table))); diff --git a/test/unit/org/apache/cassandra/db/virtual/SystemPropertiesTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SystemPropertiesTableTest.java index c8ca9ba8fa..2eb346de74 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SystemPropertiesTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SystemPropertiesTableTest.java @@ -27,7 +27,6 @@ import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import com.datastax.driver.core.ResultSet; @@ -47,12 +46,6 @@ public class SystemPropertiesTableTest extends CQLTester private SystemPropertiesTable table; - @BeforeClass - public static void setUpClass() - { - CQLTester.setUpClass(); - } - @Before public void config() { diff --git a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java index f23e50521a..1407cb8b97 100644 --- a/test/unit/org/apache/cassandra/dht/BootStrapperTest.java +++ b/test/unit/org/apache/cassandra/dht/BootStrapperTest.java @@ -18,8 +18,11 @@ package org.apache.cassandra.dht; import java.net.UnknownHostException; +import java.util.Collection; import java.util.List; import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; import com.google.common.base.Predicate; import com.google.common.base.Predicates; @@ -30,37 +33,45 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.RangeStreamer.FetchReplica; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.gms.IFailureDetectionEventListener; import org.apache.cassandra.gms.IFailureDetector; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.StreamOperation; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.utils.Pair; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class BootStrapperTest { static IPartitioner oldPartitioner; - static Predicate originalAlivePredicate = RangeStreamer.ALIVE_PREDICATE; + @BeforeClass public static void setup() throws ConfigurationException { DatabaseDescriptor.daemonInitialization(); oldPartitioner = StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); SchemaLoader.startGossiper(); - SchemaLoader.prepareServer(); SchemaLoader.schemaDefinition("BootStrapperTest"); RangeStreamer.ALIVE_PREDICATE = Predicates.alwaysTrue(); + ServerTestUtils.markCMS(); } @AfterClass @@ -73,9 +84,11 @@ public class BootStrapperTest @Test public void testSourceTargetComputation() throws UnknownHostException { - final int[] clusterSizes = new int[] { 1, 3, 5, 10, 100}; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) + final int[] clusterSizes = new int[] { 1, 3, 5, 10, 100 }; + for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces().names()) { + if (keyspaceName.equals(SchemaConstants.METADATA_KEYSPACE_NAME)) + continue; int replicationFactor = Keyspace.open(keyspaceName).getReplicationStrategy().getReplicationFactor().allReplicas; for (int clusterSize : clusterSizes) if (clusterSize >= replicationFactor) @@ -85,14 +98,11 @@ public class BootStrapperTest private RangeStreamer testSourceTargetComputation(String keyspaceName, int numOldNodes, int replicationFactor) throws UnknownHostException { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - + ServerTestUtils.resetCMS(); generateFakeEndpoints(numOldNodes); - Token myToken = tmd.partitioner.getRandomToken(); - InetAddressAndPort myEndpoint = InetAddressAndPort.getByName("127.0.0.1"); + ClusterMetadata metadata = ClusterMetadata.current(); - assertEquals(numOldNodes, tmd.sortedTokens().size()); + assertEquals(numOldNodes, metadata.tokenMap.tokens().size()); IFailureDetector mockFailureDetector = new IFailureDetector() { public boolean isAlive(InetAddressAndPort ep) @@ -107,15 +117,44 @@ public class BootStrapperTest public void remove(InetAddressAndPort ep) { throw new UnsupportedOperationException(); } public void forceConviction(InetAddressAndPort ep) { throw new UnsupportedOperationException(); } }; - RangeStreamer s = new RangeStreamer(tmd, null, myEndpoint, StreamOperation.BOOTSTRAP, true, DatabaseDescriptor.getEndpointSnitch(), new StreamStateStore(), mockFailureDetector, false, 1); + + Token myToken = metadata.partitioner.getRandomToken(); + InetAddressAndPort myEndpoint = InetAddressAndPort.getByName("127.0.0.1"); + NodeId newNode = ClusterMetadataTestHelper.register(myEndpoint); + ClusterMetadataTestHelper.JoinProcess join = ClusterMetadataTestHelper.lazyJoin(myEndpoint, myToken); + join.prepareJoin(); + metadata = ClusterMetadata.current(); + BootstrapAndJoin joiningPlan = (BootstrapAndJoin) metadata.inProgressSequences.get(newNode); + Pair movements = joiningPlan.getMovementMaps(metadata); + RangeStreamer s = new RangeStreamer(metadata, + StreamOperation.BOOTSTRAP, + true, + DatabaseDescriptor.getEndpointSnitch(), + new StreamStateStore(), + mockFailureDetector, + false, + 1, + movements.left, + movements.right); + assertNotNull(Keyspace.open(keyspaceName)); - s.addRanges(keyspaceName, Keyspace.open(keyspaceName).getReplicationStrategy().getPendingAddressRanges(tmd, myToken, myEndpoint)); + s.addKeyspaceToFetch(keyspaceName); + Multimap toFetch = s.toFetch().get(keyspaceName); - - Multimap toFetch = s.toFetch().get(keyspaceName); - - // Check we get get RF new ranges in total - assertEquals(replicationFactor, toFetch.size()); + // Pre-TCM this test would always run with RangeStreamer::useStrictSourcesForRanges returning false because + // the RangeStreamer instance was constructed with a null Collection, relying on pending ranges being + // calculated in the test code, and then cloning TokenMetadata with pending tokens added to pass to + // calculateRangesToFetchWithPreferredEndpoints. Post-TCM, we calculate both relaxed and strict movements + // together and TokenMetadata is no more, so the equivalent operation now ends up using strict movements. For + // that reason, when RF includes transient replicas, toFetch will include both a transient and full source, + // hence we dedupe the ranges here. + Set> fetchRanges = toFetch.values() + .stream() + .map(fr -> fr.remote.range()) + .collect(Collectors.toSet()); + // Post CEP-21 wrapping ranges are also unwrapped at this point, so account for that when setting expectation + int expectedRangeCount = replicationFactor + (includesWraparound(fetchRanges) ? 1 : 0); + assertEquals(expectedRangeCount, fetchRanges.size()); // there isn't any point in testing the size of these collections for any specific size. When a random partitioner // is used, they will vary. @@ -124,23 +163,31 @@ public class BootStrapperTest return s; } - private void generateFakeEndpoints(int numOldNodes) throws UnknownHostException + private boolean includesWraparound(Collection> toFetch) { - generateFakeEndpoints(StorageService.instance.getTokenMetadata(), numOldNodes, 1); + long minTokenCount = toFetch.stream() + .filter(r -> r.left.isMinimum() || r.right.isMinimum()) + .count(); + assertTrue("Ranges to fetch should either include both or neither parts of normalised wrapping range", + minTokenCount % 2 == 0); + return minTokenCount > 0; } - private void generateFakeEndpoints(TokenMetadata tmd, int numOldNodes, int numVNodes) throws UnknownHostException + private void generateFakeEndpoints(int numOldNodes) throws UnknownHostException { - tmd.clearUnsafe(); - generateFakeEndpoints(tmd, numOldNodes, numVNodes, "0", "0"); + generateFakeEndpoints(numOldNodes, 1); + } + + private void generateFakeEndpoints(int numOldNodes, int numVNodes) throws UnknownHostException + { + generateFakeEndpoints(numOldNodes, numVNodes, "0", "0"); } Random rand = new Random(1); - private void generateFakeEndpoints(TokenMetadata tmd, int numOldNodes, int numVNodes, String dc, String rack) throws UnknownHostException + private void generateFakeEndpoints(int numOldNodes, int numVNodes, String dc, String rack) throws UnknownHostException { - IPartitioner p = tmd.partitioner; - + IPartitioner p = ClusterMetadata.current().partitioner; for (int i = 1; i <= numOldNodes; i++) { // leave .1 for myEndpoint @@ -149,7 +196,7 @@ public class BootStrapperTest for (int j = 0; j < numVNodes; ++j) tokens.add(p.getRandomToken(rand)); - tmd.updateNormalTokens(tokens, addr); + ClusterMetadataTestHelper.addEndpoint(addr, tokens); } } diff --git a/test/unit/org/apache/cassandra/dht/LengthPartitioner.java b/test/unit/org/apache/cassandra/dht/LengthPartitioner.java index 24671e3856..07e40c571c 100644 --- a/test/unit/org/apache/cassandra/dht/LengthPartitioner.java +++ b/test/unit/org/apache/cassandra/dht/LengthPartitioner.java @@ -22,8 +22,8 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.ThreadLocalRandom; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.BufferDecoratedKey; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.marshal.AbstractType; diff --git a/test/unit/org/apache/cassandra/dht/tokenallocator/TokenAllocationTest.java b/test/unit/org/apache/cassandra/dht/tokenallocator/TokenAllocationTest.java index ac372e4827..f0e0cda24d 100644 --- a/test/unit/org/apache/cassandra/dht/tokenallocator/TokenAllocationTest.java +++ b/test/unit/org/apache/cassandra/dht/tokenallocator/TokenAllocationTest.java @@ -21,32 +21,33 @@ package org.apache.cassandra.dht.tokenallocator; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Collection; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Random; -import java.util.UUID; +import java.util.Set; -import com.google.common.collect.Lists; import org.apache.commons.math3.stat.descriptive.SummaryStatistics; import org.junit.AfterClass; import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.RackInferringSnitch; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; @@ -58,24 +59,34 @@ public class TokenAllocationTest static Random rand = new Random(1); @BeforeClass - public static void setup() throws ConfigurationException + public static void beforeClass() throws ConfigurationException { DatabaseDescriptor.daemonInitialization(); oldPartitioner = StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); - SchemaLoader.startGossiper(); - SchemaLoader.prepareServer(); - SchemaLoader.schemaDefinition("TokenAllocationTest"); + } + + @Before + public void before() throws ConfigurationException + { + ClusterMetadataService.setInstance(ClusterMetadataTestHelper.syncInstanceForTest()); + ClusterMetadataService.instance().log().bootstrap(FBUtilities.getBroadcastAddressAndPort()); + } + + @Before + public void after() throws ConfigurationException + { + ClusterMetadataService.unsetInstance(); } @AfterClass - public static void tearDown() + public static void afterClass() { DatabaseDescriptor.setPartitionerUnsafe(oldPartitioner); } - private static TokenAllocation createForTest(TokenMetadata tokenMetadata, int replicas, int numTokens) + private static TokenAllocation createForTest(ClusterMetadata metadata, int replicas, int numTokens) { - return TokenAllocation.create(DatabaseDescriptor.getEndpointSnitch(), tokenMetadata, replicas, numTokens); + return TokenAllocation.create(DatabaseDescriptor.getEndpointSnitch(), metadata, replicas, numTokens); } @Test @@ -83,10 +94,10 @@ public class TokenAllocationTest { int vn = 16; String ks = "TokenAllocationTestKeyspace3"; - TokenMetadata tm = new TokenMetadata(); - generateFakeEndpoints(tm, 10, vn); + ClusterMetadataTestHelper.addOrUpdateKeyspace(KeyspaceMetadata.create(ks, KeyspaceParams.simple(5))); + ClusterMetadata metadata = generateFakeEndpoints(10, vn); InetAddressAndPort addr = FBUtilities.getBroadcastAddressAndPort(); - allocateTokensForKeyspace(vn, ks, tm, addr); + allocateTokensForKeyspace(vn, ks, metadata, addr); } @Test @@ -94,22 +105,21 @@ public class TokenAllocationTest { int vn = 16; int allocateTokensForLocalRf = 3; - TokenMetadata tm = new TokenMetadata(); - generateFakeEndpoints(tm, 10, vn); + ClusterMetadata metadata = generateFakeEndpoints(10, vn); InetAddressAndPort addr = FBUtilities.getBroadcastAddressAndPort(); - allocateTokensForLocalReplicationFactor(vn, allocateTokensForLocalRf, tm, addr); + allocateTokensForLocalReplicationFactor(vn, allocateTokensForLocalRf, metadata, addr); } - private Collection allocateTokensForKeyspace(int vnodes, String keyspace, TokenMetadata tm, InetAddressAndPort addr) + private Collection allocateTokensForKeyspace(int vnodes, String keyspace, ClusterMetadata metadata, InetAddressAndPort addr) { - AbstractReplicationStrategy rs = Keyspace.open(keyspace).getReplicationStrategy(); - TokenAllocation tokenAllocation = TokenAllocation.create(tm, rs, vnodes); + AbstractReplicationStrategy rs = metadata.schema.getKeyspaces().get(keyspace).get().replicationStrategy; + TokenAllocation tokenAllocation = TokenAllocation.create(metadata, rs, vnodes); return allocateAndVerify(vnodes, addr, tokenAllocation); } - private void allocateTokensForLocalReplicationFactor(int vnodes, int rf, TokenMetadata tm, InetAddressAndPort addr) + private void allocateTokensForLocalReplicationFactor(int vnodes, int rf, ClusterMetadata metadata, InetAddressAndPort addr) { - TokenAllocation tokenAllocation = createForTest(tm, rf, vnodes); + TokenAllocation tokenAllocation = createForTest(metadata, rf, vnodes); allocateAndVerify(vnodes, addr, tokenAllocation); } @@ -166,20 +176,19 @@ public class TokenAllocationTest int vn = 16; String ks = "TokenAllocationTestNTSKeyspace" + rackCount + replicas; String dc = "1"; + String otherDc = "15"; + KeyspaceMetadata keyspace = KeyspaceMetadata.create(ks, KeyspaceParams.nts(dc, replicas, otherDc, "15")); - // Register peers with expected DC for NetworkTopologyStrategy. - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.99")); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.15.0.99")); + // register these 2 nodes so that the DCs exist, otherwise the CREATE KEYSPACE will be rejected + // but don't join them, we don't assign any tokens to these nodes + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.1.0.99"), dc, Integer.toString(0)); + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.15.0.99"), otherDc, Integer.toString(0)); + ClusterMetadataTestHelper.addOrUpdateKeyspace(keyspace); - SchemaLoader.createKeyspace(ks, KeyspaceParams.nts(dc, replicas, "15", 15), SchemaLoader.standardCFMD(ks, "Standard1")); - TokenMetadata tm = StorageService.instance.getTokenMetadata(); - tm.clearUnsafe(); for (int i = 0; i < rackCount; ++i) - generateFakeEndpoints(tm, 10, vn, dc, Integer.toString(i)); + generateFakeEndpoints(10, vn, dc, Integer.toString(i)); InetAddressAndPort addr = InetAddressAndPort.getByName("127." + dc + ".0.99"); - allocateTokensForKeyspace(vn, ks, tm, addr); + allocateTokensForKeyspace(vn, ks, ClusterMetadata.current(), addr); // Note: Not matching replication factor in second datacentre, but this should not affect us. } finally { DatabaseDescriptor.setEndpointSnitch(oldSnitch); @@ -196,25 +205,29 @@ public class TokenAllocationTest int vn = 8; int replicas = 3; int rackCount = replicas; - String ks = "TokenAllocationTestNTSKeyspaceRfEqRacks"; + String ks = "TokenAllocationTestNTSKeyspaceRfEqRack"; String dc = "1"; + String otherDc = "15"; + KeyspaceMetadata keyspace = KeyspaceMetadata.create(ks, KeyspaceParams.nts(dc, replicas, otherDc, "15")); - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.99")); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.15.0.99")); + // register these 2 nodes so that the DCs exist, otherwise the CREATE KEYSPACE will be rejected + // but don't join them, we don't assign any tokens to these nodes + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.1.0.255"), dc, Integer.toString(0)); + ClusterMetadataTestHelper.register(InetAddressAndPort.getByName("127.15.0.255"), otherDc, Integer.toString(0)); + ClusterMetadataTestHelper.addOrUpdateKeyspace(keyspace); - SchemaLoader.createKeyspace(ks, KeyspaceParams.nts(dc, replicas, "15", 15), SchemaLoader.standardCFMD(ks, "Standard1")); int base = 5; for (int i = 0; i < rackCount; ++i) - generateFakeEndpoints(metadata, base << i, vn, dc, Integer.toString(i)); // unbalanced racks + generateFakeEndpoints(base << i, vn, dc, Integer.toString(i)); // unbalanced racks int cnt = 5; for (int i = 0; i < cnt; ++i) { InetAddressAndPort endpoint = InetAddressAndPort.getByName("127." + dc + ".0." + (99 + i)); - Collection tokens = allocateTokensForKeyspace(vn, ks, metadata, endpoint); - metadata.updateNormalTokens(tokens, endpoint); + Collection tokens = allocateTokensForKeyspace(vn, ks, ClusterMetadata.current(), endpoint); + + ClusterMetadataTestHelper.register(endpoint, dc, Integer.toString(0)); + ClusterMetadataTestHelper.join(endpoint, new HashSet<>(tokens)); } double target = 1.0 / (base + cnt); @@ -247,12 +260,10 @@ public class TokenAllocationTest { final int TOKENS = 16; - TokenMetadata tokenMetadata = new TokenMetadata(); - generateFakeEndpoints(tokenMetadata, 10, TOKENS); + ClusterMetadata metadata = generateFakeEndpoints(10, TOKENS); - // Do not clone token metadata so tokens allocated by different allocators are reflected on the parent TokenMetadata - TokenAllocation rf2Allocator = createForTest(tokenMetadata, 2, TOKENS); - TokenAllocation rf3Allocator = createForTest(tokenMetadata, 3, TOKENS); + TokenAllocation rf2Allocator = createForTest(metadata, 2, TOKENS); + TokenAllocation rf3Allocator = createForTest(metadata, 3, TOKENS); SummaryStatistics rf2StatsBefore = rf2Allocator.getAllocationRingOwnership(FBUtilities.getBroadcastAddressAndPort()); SummaryStatistics rf3StatsBefore = rf3Allocator.getAllocationRingOwnership(FBUtilities.getBroadcastAddressAndPort()); @@ -263,9 +274,9 @@ public class TokenAllocationTest for (int i=11; i<=20; ++i) { InetAddressAndPort endpoint = InetAddressAndPort.getByName("127.0.0." + (i + 1)); - Collection tokens = current.allocate(endpoint); - // Update tokens on next to verify ownership calculation below - next.tokenMetadata.updateNormalTokens(tokens, endpoint); + Set tokens = new HashSet<>(current.allocate(endpoint)); + ClusterMetadataTestHelper.register(endpoint, "datacenter" + 1, "rack" + 1); + ClusterMetadataTestHelper.join(endpoint, tokens); TokenAllocation tmp = current; current = next; next = tmp; @@ -286,25 +297,27 @@ public class TokenAllocationTest } } - private void generateFakeEndpoints(TokenMetadata tmd, int numOldNodes, int numVNodes) throws UnknownHostException + private ClusterMetadata generateFakeEndpoints(int numOldNodes, int numVNodes) throws UnknownHostException { - tmd.clearUnsafe(); - generateFakeEndpoints(tmd, numOldNodes, numVNodes, "0", "0"); + return generateFakeEndpoints(numOldNodes, numVNodes, "0", "0"); } - private void generateFakeEndpoints(TokenMetadata tmd, int nodes, int vnodes, String dc, String rack) throws UnknownHostException + private ClusterMetadata generateFakeEndpoints(int nodes, int vnodes, String dc, String rack) throws UnknownHostException { System.out.printf("Adding %d nodes to dc=%s, rack=%s.%n", nodes, dc, rack); - IPartitioner p = tmd.partitioner; + ClusterMetadata metadata = ClusterMetadata.current(); + IPartitioner p = metadata.tokenMap.partitioner(); for (int i = 1; i <= nodes; i++) { // leave .1 for myEndpoint InetAddressAndPort addr = InetAddressAndPort.getByName("127." + dc + '.' + rack + '.' + (i + 1)); - List tokens = Lists.newArrayListWithCapacity(vnodes); + ClusterMetadataTestHelper.register(addr, dc, rack); + Set tokens = new HashSet<>(vnodes); for (int j = 0; j < vnodes; ++j) tokens.add(p.getRandomToken(rand)); - tmd.updateNormalTokens(tokens, addr); + ClusterMetadataTestHelper.join(addr, tokens); } + return metadata; } } diff --git a/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java b/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java deleted file mode 100644 index 298549e394..0000000000 --- a/test/unit/org/apache/cassandra/gms/ExpireEndpointTest.java +++ /dev/null @@ -1,65 +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.gms; - -import java.net.UnknownHostException; -import java.util.UUID; - -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -public class ExpireEndpointTest -{ - @BeforeClass - public static void setup() - { - DatabaseDescriptor.daemonInitialization(); - } - - @Test - public void testExpireEndpoint() throws UnknownHostException - { - InetAddressAndPort hostAddress = InetAddressAndPort.getByName("127.0.0.2"); - UUID hostId = UUID.randomUUID(); - long expireTime = System.currentTimeMillis() - 1; - - Gossiper.instance.initializeNodeUnsafe(hostAddress, hostId, 1); - - EndpointState endpointState = Gossiper.instance.getEndpointStateForEndpoint(hostAddress); - Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.markDead(hostAddress, endpointState)); - endpointState.addApplicationState(ApplicationState.STATUS_WITH_PORT, StorageService.instance.valueFactory.removedNonlocal(hostId, expireTime)); - Gossiper.instance.addExpireTimeForEndpoint(hostAddress, expireTime); - - assertTrue("Expiring endpoint not unreachable before status check", Gossiper.instance.getUnreachableMembers().contains(hostAddress)); - - Gossiper.instance.doStatusCheck(); - - assertFalse("Expired endpoint still part of live members", Gossiper.instance.getLiveMembers().contains(hostAddress)); - assertFalse("Expired endpoint still part of unreachable members", Gossiper.instance.getUnreachableMembers().contains(hostAddress)); - assertNull("Expired endpoint still contain endpoint state", Gossiper.instance.getEndpointStateForEndpoint(hostAddress)); - } -} diff --git a/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java b/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java index 8ec3dae913..b0b6c447fc 100644 --- a/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java +++ b/test/unit/org/apache/cassandra/gms/FailureDetectorTest.java @@ -20,22 +20,21 @@ package org.apache.cassandra.gms; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; -import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import static org.apache.cassandra.config.CassandraRelevantProperties.MAX_LOCAL_PAUSE_IN_MS; import static org.junit.Assert.assertFalse; @@ -45,22 +44,17 @@ public class FailureDetectorTest @BeforeClass public static void setup() { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(RandomPartitioner.instance); // slow unit tests can cause problems with FailureDetector's GC pause handling MAX_LOCAL_PAUSE_IN_MS.setLong(20000); - - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); } @Test public void testConvictAfterLeft() throws UnknownHostException { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - IPartitioner partitioner = new RandomPartitioner(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - ArrayList endpointTokens = new ArrayList<>(); ArrayList keyTokens = new ArrayList<>(); List hosts = new ArrayList<>(); @@ -70,18 +64,16 @@ public class FailureDetectorTest DatabaseDescriptor.setPhiConvictThreshold(0); // create a ring of 2 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 3); + Util.createInitialRing(endpointTokens, keyTokens, hosts, hostIds, 3); InetAddressAndPort leftHost = hosts.get(1); FailureDetector.instance.report(leftHost); - // trigger handleStateLeft in StorageService - ss.onChange(leftHost, ApplicationState.STATUS_WITH_PORT, - valueFactory.left(Collections.singleton(endpointTokens.get(1)), Gossiper.computeExpireTime())); + ClusterMetadataTestHelper.removeEndpoint(leftHost, true); // confirm that handleStateLeft was called and leftEndpoint was removed from TokenMetadata - assertFalse("Left endpoint not removed from TokenMetadata", tmd.isMember(leftHost)); + assertFalse("Left endpoint not removed from ClusterMetadata", ClusterMetadata.current().directory.allJoinedEndpoints().contains(leftHost)); // confirm the FD's history for leftHost didn't get wiped by status jump to LEFT FailureDetector.instance.interpret(leftHost); diff --git a/test/unit/org/apache/cassandra/gms/GossipShutdownTest.java b/test/unit/org/apache/cassandra/gms/GossipShutdownTest.java index 7bfa2ad0d4..d8f92fe53e 100644 --- a/test/unit/org/apache/cassandra/gms/GossipShutdownTest.java +++ b/test/unit/org/apache/cassandra/gms/GossipShutdownTest.java @@ -30,6 +30,8 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.assertj.core.api.Assertions; public class GossipShutdownTest @@ -40,7 +42,9 @@ public class GossipShutdownTest @BeforeClass public static void beforeClass() { - DatabaseDescriptor.clientInitialization(); + DatabaseDescriptor.daemonInitialization(); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); } @Test diff --git a/test/unit/org/apache/cassandra/gms/GossiperTest.java b/test/unit/org/apache/cassandra/gms/GossiperTest.java index 461483643a..4362021e57 100644 --- a/test/unit/org/apache/cassandra/gms/GossiperTest.java +++ b/test/unit/org/apache/cassandra/gms/GossiperTest.java @@ -18,6 +18,7 @@ package org.apache.cassandra.gms; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; @@ -36,29 +37,27 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.SeedProvider; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.apache.cassandra.utils.CassandraGenerators; -import org.apache.cassandra.utils.CassandraVersion; -import org.apache.cassandra.utils.FBUtilities; import org.assertj.core.api.Assertions; import org.quicktheories.core.Gen; import org.quicktheories.impl.Constraint; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -66,30 +65,29 @@ import static org.quicktheories.QuickTheory.qt; public class GossiperTest { - static - { - GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); - DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - } - - private static final CassandraVersion CURRENT_VERSION = new CassandraVersion(FBUtilities.getReleaseVersionString()); - - static final IPartitioner partitioner = new RandomPartitioner(); - StorageService ss = StorageService.instance; - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); List hosts = new ArrayList<>(); List hostIds = new ArrayList<>(); - private SeedProvider originalSeedProvider; + private IPartitioner partitioner; + + @BeforeClass + public static void init() throws IOException + { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(RandomPartitioner.instance); + ServerTestUtils.cleanupAndLeaveDirs(); + GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); + CommitLog.instance.start(); + } @Before public void setup() { - tmd.clearUnsafe(); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); originalSeedProvider = DatabaseDescriptor.getSeedProvider(); + partitioner = DatabaseDescriptor.getPartitioner(); } @After @@ -120,51 +118,10 @@ public class GossiperTest assert ApplicationState.X10 == ApplicationState.X10; } - @Test - public void testHasVersion3Nodes() throws Exception - { - Gossiper.instance.start(0); - Gossiper.instance.expireUpgradeFromVersion(); - - VersionedValue.VersionedValueFactory factory = new VersionedValue.VersionedValueFactory(null); - EndpointState es = new EndpointState((HeartBeatState) null); - es.addApplicationState(ApplicationState.RELEASE_VERSION, factory.releaseVersion(CURRENT_VERSION.toString())); - Gossiper.instance.endpointStateMap.put(InetAddressAndPort.getByName("127.0.0.1"), es); - Gossiper.instance.liveEndpoints.add(InetAddressAndPort.getByName("127.0.0.1")); - - - es = new EndpointState((HeartBeatState) null); - es.addApplicationState(ApplicationState.RELEASE_VERSION, factory.releaseVersion("3.11.3")); - Gossiper.instance.endpointStateMap.put(InetAddressAndPort.getByName("127.0.0.2"), es); - Gossiper.instance.liveEndpoints.add(InetAddressAndPort.getByName("127.0.0.2")); - - es = new EndpointState((HeartBeatState) null); - es.addApplicationState(ApplicationState.RELEASE_VERSION, factory.releaseVersion("3.0.0")); - Gossiper.instance.endpointStateMap.put(InetAddressAndPort.getByName("127.0.0.3"), es); - Gossiper.instance.liveEndpoints.add(InetAddressAndPort.getByName("127.0.0.3")); - - assertFalse(Gossiper.instance.upgradeFromVersionSupplier.get().value().compareTo(new CassandraVersion("3.0")) < 0); - assertTrue(Gossiper.instance.upgradeFromVersionSupplier.get().value().compareTo(new CassandraVersion("3.1")) < 0); - assertTrue(Gossiper.instance.hasMajorVersion3Nodes()); - - Gossiper.instance.endpointStateMap.remove(InetAddressAndPort.getByName("127.0.0.3")); - Gossiper.instance.liveEndpoints.remove(InetAddressAndPort.getByName("127.0.0.3")); - - assertFalse(Gossiper.instance.upgradeFromVersionSupplier.get().value().compareTo(new CassandraVersion("3.0")) < 0); - assertFalse(Gossiper.instance.upgradeFromVersionSupplier.get().value().compareTo(new CassandraVersion("3.1")) < 0); - assertTrue(Gossiper.instance.upgradeFromVersionSupplier.get().value().compareTo(new CassandraVersion("3.12")) < 0); - assertTrue(Gossiper.instance.hasMajorVersion3Nodes()); - - Gossiper.instance.endpointStateMap.remove(InetAddressAndPort.getByName("127.0.0.2")); - Gossiper.instance.liveEndpoints.remove(InetAddressAndPort.getByName("127.0.0.2")); - - assertEquals(SystemKeyspace.CURRENT_VERSION, Gossiper.instance.upgradeFromVersionSupplier.get().value()); - } - @Test public void testLargeGenerationJump() throws UnknownHostException, InterruptedException { - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 2); + Util.initGossipTokens(partitioner, endpointTokens, hosts, hostIds, 2); try { InetAddressAndPort remoteHostAddress = hosts.get(1); @@ -172,7 +129,7 @@ public class GossiperTest EndpointState initialRemoteState = Gossiper.instance.getEndpointStateForEndpoint(remoteHostAddress); HeartBeatState initialRemoteHeartBeat = initialRemoteState.getHeartBeatState(); - //Util.createInitialRing should have initialized remoteHost's HeartBeatState's generation to 1 + //Util.initGossipTokens should have initialized remoteHost's HeartBeatState's generation to 1 assertEquals(initialRemoteHeartBeat.getGeneration(), 1); HeartBeatState proposedRemoteHeartBeat = new HeartBeatState(initialRemoteHeartBeat.getGeneration() + Gossiper.MAX_GENERATION_DIFFERENCE + 1); @@ -211,7 +168,7 @@ public class GossiperTest new VersionedValue.VersionedValueFactory(DatabaseDescriptor.getPartitioner()); SimpleStateChangeListener stateChangeListener = null; - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 2); + Util.initGossipTokens(partitioner, endpointTokens, hosts, hostIds, 2); try { InetAddressAndPort remoteHostAddress = hosts.get(1); @@ -219,7 +176,7 @@ public class GossiperTest EndpointState initialRemoteState = Gossiper.instance.getEndpointStateForEndpoint(remoteHostAddress); HeartBeatState initialRemoteHeartBeat = initialRemoteState.getHeartBeatState(); - //Util.createInitialRing should have initialized remoteHost's HeartBeatState's generation to 1 + //Util.initGossipTokens should have initialized remoteHost's HeartBeatState's generation to 1 assertEquals(initialRemoteHeartBeat.getGeneration(), 1); HeartBeatState proposedRemoteHeartBeat = new HeartBeatState(initialRemoteHeartBeat.getGeneration()); @@ -253,6 +210,8 @@ public class GossiperTest // The following state change should only update heartbeat without updating the TOKENS state Gossiper.instance.applyStateLocally(ImmutableMap.of(remoteHostAddress, proposedRemoteState)); + // TOKENS are maintained in ClusterMetadata. Although present in ApplicationState for backwards + // compatibility, they don't trigger state changes any more. assertEquals(1, stateChangedNum); actualRemoteHeartBeat = Gossiper.instance.getEndpointStateForEndpoint(remoteHostAddress).getHeartBeatState(); @@ -369,14 +328,14 @@ public class GossiperTest VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(DatabaseDescriptor.getPartitioner()); - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 2); + Util.initGossipTokens(partitioner, endpointTokens, hosts, hostIds, 2); SimpleStateChangeListener stateChangeListener = null; try { InetAddressAndPort remoteHostAddress = hosts.get(1); EndpointState initialRemoteState = Gossiper.instance.getEndpointStateForEndpoint(remoteHostAddress); HeartBeatState initialRemoteHeartBeat = initialRemoteState.getHeartBeatState(); - //Util.createInitialRing should have initialized remoteHost's HeartBeatState's generation to 1 + //Util.initGossipTokens should have initialized remoteHost's HeartBeatState's generation to 1 assertEquals(initialRemoteHeartBeat.getGeneration(), 1); // Test begins @@ -415,6 +374,7 @@ public class GossiperTest notificationCount.getAndIncrement(); fail("It should not fire notification for STATUS"); }); + Gossiper.instance.applyStateLocally(ImmutableMap.of(remoteHostAddress, proposedRemoteState)); assertEquals("Expect exact 2 notifications with the test setup", 2, notificationCount.get()); diff --git a/test/unit/org/apache/cassandra/gms/NewGossiperTest.java b/test/unit/org/apache/cassandra/gms/NewGossiperTest.java new file mode 100644 index 0000000000..54cd8fe43a --- /dev/null +++ b/test/unit/org/apache/cassandra/gms/NewGossiperTest.java @@ -0,0 +1,154 @@ +/* + * 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.gms; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import com.google.common.collect.Sets; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.ConnectionType; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.tcm.compatibility.GossipHelper; +import org.apache.cassandra.utils.concurrent.Future; + +import static org.apache.cassandra.gms.ApplicationState.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NewGossiperTest +{ + static Set fakePeers = new HashSet<>(); + static + { + for (int i = 1; i < 21; i++) // we require 10% of nodes to respond, so for two responses we need a cluster with 20 nodes + fakePeers.add(InetAddressAndPort.getByNameUnchecked("127.0.0." + i)); + } + + @Test + public void mergeTest() throws InterruptedException, ExecutionException + { + ServerTestUtils.prepareServerNoRegister(); + + Random r = new Random(System.currentTimeMillis()); + for (int i = 0; i < 5000; i++) + { + NewGossiper.ShadowRoundHandler srh = new NewGossiper.ShadowRoundHandler(fakePeers, new NoOpMessageDelivery()); + Future> states = srh.doShadowRound(); + Map firstResp = buildEpstates(fakePeers, r); + srh.onAck(firstResp); + Map secondResp = buildEpstates(fakePeers, r); + srh.onAck(secondResp); + Map result = states.get(); + verifyResult(result, firstResp, secondResp); + } + } + + private static void verifyResult(Map result, + Map firstResp, + Map secondResp) + { + assertEquals(Sets.union(firstResp.keySet(), secondResp.keySet()), result.keySet()); + assertTrue(GossipHelper.isValidForClusterMetadata(result)); + + for (InetAddressAndPort ep : Sets.union(firstResp.keySet(), secondResp.keySet())) + { + EndpointState first = firstResp.get(ep); + EndpointState second = secondResp.get(ep); + assertTrue(first != null || second != null); + if (first == null) + assertEquals(second, result.get(ep)); + else if (second == null) + assertEquals(first, result.get(ep)); + else if (first.getHeartBeatState().getGeneration() > second.getHeartBeatState().getGeneration()) + assertEquals(first, result.get(ep)); + else if (first.getHeartBeatState().getGeneration() < second.getHeartBeatState().getGeneration()) + assertEquals(second, result.get(ep)); + else // equal generations + { + if (first.isSupersededBy(second)) + assertEquals(second, result.get(ep)); + else if (second.isSupersededBy(first)) + assertEquals(first, result.get(ep)); + else + assertEquals(Gossiper.getMaxEndpointStateVersion(first), Gossiper.getMaxEndpointStateVersion(second)); + } + } + } + + private static Map buildEpstates(Set fakePeers, Random r) + { + Map epstates = new HashMap<>(); + + VersionedValue.VersionedValueFactory vvf = new VersionedValue.VersionedValueFactory(Murmur3Partitioner.instance, () -> r.nextInt(500)); + for (InetAddressAndPort endpoint : fakePeers) + { + if (r.nextDouble() < 0.001) + continue; + EndpointState epstate = new EndpointState(new HeartBeatState(r.nextInt(50), r.nextInt(100))); + epstate.addApplicationState(TOKENS, vvf.tokens(Collections.singleton(new Murmur3Partitioner.LongToken(r.nextInt(5000))))); + epstate.addApplicationState(HOST_ID, vvf.hostId(UUID.randomUUID())); + epstate.addApplicationState(DC, vvf.datacenter("dc" + r.nextInt(5))); + epstate.addApplicationState(RACK, vvf.rack("rack" + r.nextInt(5))); + epstate.addApplicationState(RELEASE_VERSION, vvf.releaseVersion("5.0."+r.nextInt())); + epstates.put(endpoint, epstate); + } + return epstates; + } + + private static class NoOpMessageDelivery implements MessageDelivery + { + public void send(Message message, InetAddressAndPort to) {} + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) {} + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) {} + public Future> sendWithResult(Message message, InetAddressAndPort to) {return null;} + public void respond(V response, Message message) {} + } + + @Test + public void testIncompleteState() + { + Map epstates = buildEpstates(Collections.singleton(fakePeers.iterator().next()), new Random()); + assertTrue(GossipHelper.isValidForClusterMetadata(epstates)); + Map brokenEpstates = new HashMap<>(); + for (Map.Entry entry : epstates.entrySet()) + { + EndpointState epstate = new EndpointState(entry.getValue().getHeartBeatState()); + for (Map.Entry vals : entry.getValue().states()) + { + if (vals.getKey() != TOKENS) + epstate.addApplicationState(vals.getKey(), vals.getValue()); + } + } + assertFalse(GossipHelper.isValidForClusterMetadata(brokenEpstates)); // does not contain TOKEN anymore + } +} diff --git a/test/unit/org/apache/cassandra/gms/PendingRangeCalculatorServiceTest.java b/test/unit/org/apache/cassandra/gms/PendingRangeCalculatorServiceTest.java deleted file mode 100644 index 1f7587668e..0000000000 --- a/test/unit/org/apache/cassandra/gms/PendingRangeCalculatorServiceTest.java +++ /dev/null @@ -1,134 +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.gms; - -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.locks.ReentrantLock; - -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.dht.ByteOrderedPartitioner; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; -import org.jboss.byteman.contrib.bmunit.BMRule; -import org.jboss.byteman.contrib.bmunit.BMUnitRunner; - -import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - - -/** - * Test for "Gossip blocks on startup when another node is bootstrapping" (CASSANDRA-12281). - */ -@RunWith(BMUnitRunner.class) -public class PendingRangeCalculatorServiceTest -{ - static ReentrantLock calculationLock = new ReentrantLock(); - - @BeforeClass - public static void setUp() throws ConfigurationException - { - GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); - SchemaLoader.prepareServer(); - StorageService.instance.initServer(); - } - - @Test - @BMRule(name = "Block pending range calculation", - targetClass = "TokenMetadata", - targetMethod = "calculatePendingRanges", - targetLocation = "AT INVOKE org.apache.cassandra.locator.AbstractReplicationStrategy.getAddressReplicas", - action = "org.apache.cassandra.gms.PendingRangeCalculatorServiceTest.calculationLock.lock()") - public void testDelayedResponse() throws UnknownHostException, InterruptedException - { - InetAddressAndPort otherNodeAddr = InetAddressAndPort.getByName("127.0.0.2"); - UUID otherHostId = UUID.randomUUID(); - - // introduce node for first major state change - Gossiper.instance.applyStateLocally(getStates(otherNodeAddr, otherHostId, 1, false)); - - // acquire lock to block pending range calculation via byteman - calculationLock.lock(); - try - { - // spawn thread that will trigger handling of a bootstrap state change which in turn will trigger - // the pending range calculation that will be blocked by our lock - Thread t1 = new Thread() - { - public void run() - { - Gossiper.instance.applyStateLocally(getStates(otherNodeAddr, otherHostId, 2, true)); - } - }; - t1.start(); - - // busy-spin until t1 is blocked by lock - while (!calculationLock.hasQueuedThreads()) ; - - // trigger further state changes in case we don't want the blocked thread from the - // expensive range calculation to block us here as well - Thread t2 = new Thread() - { - public void run() - { - Gossiper.instance.applyStateLocally(getStates(otherNodeAddr, otherHostId, 3, false)); - Gossiper.instance.applyStateLocally(getStates(otherNodeAddr, otherHostId, 4, false)); - Gossiper.instance.applyStateLocally(getStates(otherNodeAddr, otherHostId, 5, false)); - } - }; - t2.start(); - t2.join(2000); - assertFalse("Thread still blocked by pending range calculation", t2.isAlive()); - assertEquals(5, Gossiper.instance.getEndpointStateForEndpoint(otherNodeAddr).getHeartBeatState().getHeartBeatVersion()); - } - finally - { - calculationLock.unlock(); - } - } - - private Map getStates(InetAddressAndPort otherNodeAddr, UUID hostId, int ver, boolean bootstrapping) - { - HeartBeatState hb = new HeartBeatState(1, ver); - EndpointState state = new EndpointState(hb); - Collection tokens = new ArrayList<>(); - - tokens.add(new ByteOrderedPartitioner.BytesToken(new byte[]{1,2,3})); - state.addApplicationState(ApplicationState.TOKENS, StorageService.instance.valueFactory.tokens(tokens)); - state.addApplicationState(ApplicationState.STATUS, bootstrapping ? - StorageService.instance.valueFactory.bootstrapping(tokens) : StorageService.instance.valueFactory.normal(tokens)); - state.addApplicationState(ApplicationState.HOST_ID, StorageService.instance.valueFactory.hostId(hostId)); - state.addApplicationState(ApplicationState.NET_VERSION, StorageService.instance.valueFactory.networkVersion()); - - Map states = new HashMap<>(); - states.put(otherNodeAddr, state); - return states; - } -} diff --git a/test/unit/org/apache/cassandra/gms/SerializationsTest.java b/test/unit/org/apache/cassandra/gms/SerializationsTest.java index 0422ac0017..1dc44345de 100644 --- a/test/unit/org/apache/cassandra/gms/SerializationsTest.java +++ b/test/unit/org/apache/cassandra/gms/SerializationsTest.java @@ -18,19 +18,6 @@ */ package org.apache.cassandra.gms; -import org.apache.cassandra.AbstractSerializationsTester; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.io.util.DataOutputStreamPlus; -import org.apache.cassandra.io.util.FileInputStreamPlus; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; - -import org.junit.BeforeClass; -import org.junit.Test; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -38,12 +25,27 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.AbstractSerializationsTester; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.io.util.DataOutputStreamPlus; +import org.apache.cassandra.io.util.FileInputStreamPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.FBUtilities; + public class SerializationsTest extends AbstractSerializationsTester { @BeforeClass public static void initDD() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); } private void testEndpointStateWrite() throws IOException @@ -84,7 +86,7 @@ public class SerializationsTest extends AbstractSerializationsTester GossipDigestAck ack = new GossipDigestAck(Statics.Digests, states); GossipDigestAck2 ack2 = new GossipDigestAck2(states); GossipDigestSyn syn = new GossipDigestSyn("Not a real cluster name", - StorageService.instance.getTokenMetadata().partitioner.getClass().getCanonicalName(), + ClusterMetadata.current().tokenMap.partitioner().getClass().getCanonicalName(), Statics.Digests); DataOutputStreamPlus out = getOutput("gms.Gossip.bin"); @@ -123,7 +125,7 @@ public class SerializationsTest extends AbstractSerializationsTester { private static HeartBeatState HeartbeatSt = new HeartBeatState(101, 201); private static EndpointState EndpointSt = new EndpointState(HeartbeatSt); - private static IPartitioner partitioner = StorageService.instance.getTokenMetadata().partitioner; + private static IPartitioner partitioner = ClusterMetadata.current().tokenMap.partitioner(); private static VersionedValue.VersionedValueFactory vvFact = new VersionedValue.VersionedValueFactory(partitioner); private static VersionedValue vv0 = vvFact.load(23d); private static VersionedValue vv1 = vvFact.bootstrapping(Collections.singleton(partitioner.getRandomToken())); diff --git a/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java b/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java deleted file mode 100644 index 33d9a1206a..0000000000 --- a/test/unit/org/apache/cassandra/gms/ShadowRoundTest.java +++ /dev/null @@ -1,218 +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.gms; - -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.db.commitlog.CommitLog; -import org.apache.cassandra.distributed.shared.WithProperties; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.IEndpointSnitch; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.PropertyFileSnitch; -import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.MockMessagingService; -import org.apache.cassandra.net.MockMessagingSpy; -import org.apache.cassandra.net.Verb; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; - -import static org.apache.cassandra.config.CassandraRelevantProperties.AUTO_BOOTSTRAP; -import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; -import static org.apache.cassandra.net.MockMessagingService.verb; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class ShadowRoundTest -{ - private static final Logger logger = LoggerFactory.getLogger(ShadowRoundTest.class); - - @BeforeClass - public static void setUp() throws ConfigurationException - { - CASSANDRA_CONFIG.setString("cassandra-seeds.yaml"); - - DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - IEndpointSnitch snitch = new PropertyFileSnitch(); - DatabaseDescriptor.setEndpointSnitch(snitch); - Keyspace.setInitialized(); - } - - @After - public void cleanup() - { - MockMessagingService.cleanup(); - } - - @Test - public void testDelayedResponse() - { - Gossiper.instance.buildSeedsList(); - int noOfSeeds = Gossiper.instance.seeds.size(); - - final AtomicBoolean ackSend = new AtomicBoolean(false); - MockMessagingSpy spySyn = MockMessagingService.when(verb(Verb.GOSSIP_DIGEST_SYN)) - .respondN((msgOut, to) -> - { - // ACK once to finish shadow round, then busy-spin until gossiper has been enabled - // and then respond with remaining ACKs from other seeds - if (!ackSend.compareAndSet(false, true)) - { - while (!Gossiper.instance.isEnabled()) ; - } - - HeartBeatState hb = new HeartBeatState(123, 456); - EndpointState state = new EndpointState(hb); - GossipDigestAck payload = new GossipDigestAck( - Collections.singletonList(new GossipDigest(to, hb.getGeneration(), hb.getHeartBeatVersion())), - Collections.singletonMap(to, state)); - - logger.debug("Simulating digest ACK response"); - return Message.builder(Verb.GOSSIP_DIGEST_ACK, payload) - .from(to) - .build(); - }, noOfSeeds); - - // GossipDigestAckVerbHandler will send ack2 for each ack received (after the shadow round) - MockMessagingSpy spyAck2 = MockMessagingService.when(verb(Verb.GOSSIP_DIGEST_ACK2)).dontReply(); - - // Migration request messages should not be emitted during shadow round - MockMessagingSpy spyMigrationReq = MockMessagingService.when(verb(Verb.SCHEMA_PULL_REQ)).dontReply(); - - try - { - StorageService.instance.initServer(); - } - catch (Exception e) - { - assertThat(e.getMessage()).startsWith("Unable to contact any seeds"); - } - - // we expect one SYN for each seed during shadow round + additional SYNs after gossiper has been enabled - assertTrue(spySyn.messagesIntercepted() > noOfSeeds); - - // we don't expect to emit any GOSSIP_DIGEST_ACK2 or SCHEMA_PULL messages - assertEquals(0, spyAck2.messagesIntercepted()); - assertEquals(0, spyMigrationReq.messagesIntercepted()); - } - - @Test - public void testBadAckInShadow() - { - final AtomicBoolean ackSend = new AtomicBoolean(false); - MockMessagingSpy spySyn = MockMessagingService.when(verb(Verb.GOSSIP_DIGEST_SYN)) - .respondN((msgOut, to) -> - { - // ACK with bad data in shadow round - if (!ackSend.compareAndSet(false, true)) - { - while (!Gossiper.instance.isEnabled()) ; - } - InetAddressAndPort junkaddr; - try - { - junkaddr = InetAddressAndPort.getByName("1.1.1.1"); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - - HeartBeatState hb = new HeartBeatState(123, 456); - EndpointState state = new EndpointState(hb); - List gDigests = new ArrayList(); - gDigests.add(new GossipDigest(FBUtilities.getBroadcastAddressAndPort(), hb.getGeneration(), hb.getHeartBeatVersion())); - gDigests.add(new GossipDigest(junkaddr, hb.getGeneration(), hb.getHeartBeatVersion())); - Map smap = new HashMap() - { - { - put(FBUtilities.getBroadcastAddressAndPort(), state); - put(junkaddr, state); - } - }; - GossipDigestAck payload = new GossipDigestAck(gDigests, smap); - - logger.debug("Simulating bad digest ACK reply"); - return Message.builder(Verb.GOSSIP_DIGEST_ACK, payload) - .from(to) - .build(); - }, 1); - - - try (WithProperties properties = new WithProperties().set(AUTO_BOOTSTRAP, false)) - { - StorageService.instance.checkForEndpointCollision(SystemKeyspace.getOrInitializeLocalHostId(), SystemKeyspace.loadHostIds().keySet()); - } - catch (Exception e) - { - assertEquals("Unable to gossip with any peers", e.getMessage()); - } - } - - @Test - public void testPreviouslyAssassinatedInShadow() - { - final AtomicBoolean ackSend = new AtomicBoolean(false); - MockMessagingSpy spySyn = MockMessagingService.when(verb(Verb.GOSSIP_DIGEST_SYN)) - .respondN((msgOut, to) -> - { - // ACK with self assassinated in shadow round - if (!ackSend.compareAndSet(false, true)) - { - while (!Gossiper.instance.isEnabled()) ; - } - HeartBeatState hb = new HeartBeatState(123, 456); - EndpointState state = new EndpointState(hb); - state.addApplicationState(ApplicationState.STATUS_WITH_PORT, VersionedValue.unsafeMakeVersionedValue(VersionedValue.STATUS_LEFT, 1)); - GossipDigestAck payload = new GossipDigestAck( - Collections.singletonList(new GossipDigest(FBUtilities.getBroadcastAddressAndPort(), hb.getGeneration(), hb.getHeartBeatVersion())), - Collections.singletonMap(FBUtilities.getBroadcastAddressAndPort(), state)); - - logger.debug("Simulating bad digest ACK reply"); - return Message.builder(Verb.GOSSIP_DIGEST_ACK, payload) - .from(to) - .build(); - }, 1); - - - try (WithProperties properties = new WithProperties().set(AUTO_BOOTSTRAP, false)) - { - StorageService.instance.checkForEndpointCollision(SystemKeyspace.getOrInitializeLocalHostId(), SystemKeyspace.loadHostIds().keySet()); - } - } - -} diff --git a/test/unit/org/apache/cassandra/hints/AlteredHints.java b/test/unit/org/apache/cassandra/hints/AlteredHints.java index 4bb6178df4..11cf5af1b9 100644 --- a/test/unit/org/apache/cassandra/hints/AlteredHints.java +++ b/test/unit/org/apache/cassandra/hints/AlteredHints.java @@ -32,8 +32,8 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.RowUpdateBuilder; import org.apache.cassandra.schema.KeyspaceParams; diff --git a/test/unit/org/apache/cassandra/hints/DTestSerializer.java b/test/unit/org/apache/cassandra/hints/DTestSerializer.java index 0221c52d44..5f20baf7a1 100644 --- a/test/unit/org/apache/cassandra/hints/DTestSerializer.java +++ b/test/unit/org/apache/cassandra/hints/DTestSerializer.java @@ -24,6 +24,7 @@ import java.util.UUID; import com.google.common.primitives.Ints; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.IVersionedAsymmetricSerializer; import org.apache.cassandra.io.util.DataInputPlus; @@ -62,10 +63,11 @@ public class DTestSerializer implements IVersionedAsymmetricSerializer stateMap = new EnumMap<>(ApplicationState.class); - stateMap.put(ApplicationState.DC, StorageService.instance.valueFactory.datacenter("cn-shanghai")); - stateMap.put(ApplicationState.RACK, StorageService.instance.valueFactory.datacenter("a")); - Gossiper.instance.getEndpointStateForEndpoint(nonlocal).addApplicationStates(stateMap); + Token t1 = ClusterMetadata.current().partitioner.getRandomToken(); + ClusterMetadataTestHelper.addEndpoint(nonlocal, t1, "cn-shanghai", "a"); assertEquals("cn-shanghai", snitch.getDatacenter(nonlocal)); assertEquals("a", snitch.getRack(nonlocal)); @@ -104,10 +88,4 @@ public class AlibabaCloudSnitchTest assertEquals("us-east", snitch.getDatacenter(local)); assertEquals("1a", snitch.getRack(local)); } - - @AfterClass - public static void tearDown() - { - StorageService.instance.stopClient(); - } } diff --git a/test/unit/org/apache/cassandra/locator/AssureSufficientLiveNodesTest.java b/test/unit/org/apache/cassandra/locator/AssureSufficientLiveNodesTest.java index 2b84a5f3cf..2bec9b8d29 100644 --- a/test/unit/org/apache/cassandra/locator/AssureSufficientLiveNodesTest.java +++ b/test/unit/org/apache/cassandra/locator/AssureSufficientLiveNodesTest.java @@ -20,7 +20,6 @@ package org.apache.cassandra.locator; import java.util.Arrays; import java.util.List; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -38,16 +37,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.schema.Tables; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.NeverSpeculativeRetryPolicy; import org.apache.cassandra.utils.FBUtilities; import org.jboss.byteman.contrib.bmunit.BMRule; @@ -82,11 +82,12 @@ public class AssureSufficientLiveNodesTest @BeforeClass public static void setUpClass() throws Throwable { - SchemaLoader.loadSchema(); + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); // Register peers with expected DC for NetworkTopologyStrategy. - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); + // TODO shouldn't require the snitch setup DatabaseDescriptor.setEndpointSnitch(new AbstractNetworkTopologySnitch() { public String getRack(InetAddressAndPort endpoint) @@ -113,8 +114,9 @@ public class AssureSufficientLiveNodesTest for (int i = 0; i < instances.size(); i++) { InetAddressAndPort ip = instances.get(i); - metadata.updateHostId(UUID.randomUUID(), ip); - metadata.updateNormalToken(new Murmur3Partitioner.LongToken(i), ip); + String dc = "datacenter" + ip.addressBytes[1]; + String rack = "rake" + ip.addressBytes[1]; + ClusterMetadataTestHelper.addEndpoint(ip, new Murmur3Partitioner.LongToken(i), dc, rack); } } @@ -266,13 +268,13 @@ public class AssureSufficientLiveNodesTest for (int i = 0; i < loopCount; i++) { // reset the keyspace - racedKs.setMetadata(initKsMeta); +// racedKs.setMetadata(initKsMeta); CountDownLatch trigger = new CountDownLatch(1); // starts 2 runnables that could race Future f1 = es.submit(() -> { Uninterruptibles.awaitUninterruptibly(trigger); // Update replication strategy - racedKs.setMetadata(alterToKsMeta); +// racedKs.setMetadata(alterToKsMeta); }); Future f2 = es.submit(() -> { Uninterruptibles.awaitUninterruptibly(trigger); diff --git a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java index 2c72c67448..012fd938ce 100644 --- a/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/CloudstackSnitchTest.java @@ -19,26 +19,20 @@ package org.apache.cassandra.locator; import java.io.IOException; -import java.util.EnumMap; -import java.util.Map; -import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DefaultCloudMetadataServiceConnector; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.Pair; -import static org.apache.cassandra.ServerTestUtils.cleanup; -import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; import static org.junit.Assert.assertEquals; @@ -55,12 +49,13 @@ public class CloudstackSnitchTest { GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - CommitLog.instance.segmentManager.awaitManagementTasksCompletion(); - mkdirs(); - cleanup(); - Keyspace.setInitialized(); - StorageService.instance.initServer(0); + ClusterMetadataTestHelper.setInstanceForTest(); + } + + @Before + public void resetCMS() + { + ServerTestUtils.resetCMS(); } @Test @@ -77,11 +72,8 @@ public class CloudstackSnitchTest InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); - Gossiper.instance.addSavedEndpoint(nonlocal); - Map stateMap = new EnumMap<>(ApplicationState.class); - stateMap.put(ApplicationState.DC, StorageService.instance.valueFactory.datacenter("ch-zrh")); - stateMap.put(ApplicationState.RACK, StorageService.instance.valueFactory.rack("2")); - Gossiper.instance.getEndpointStateForEndpoint(nonlocal).addApplicationStates(stateMap); + Token t1 = ClusterMetadata.current().partitioner.getRandomToken(); + ClusterMetadataTestHelper.addEndpoint(nonlocal, t1, "ch-zrh", "2"); assertEquals("ch-zrh", snitch.getDatacenter(nonlocal)); assertEquals("2", snitch.getRack(nonlocal)); @@ -107,10 +99,4 @@ public class CloudstackSnitchTest assertEquals("us-east", snitch.getDatacenter(local)); assertEquals("1a", snitch.getRack(local)); } - - @AfterClass - public static void tearDown() - { - StorageService.instance.stopClient(); - } } diff --git a/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java b/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java index 98a9d16518..310be2a6b0 100644 --- a/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/DynamicEndpointSnitchTest.java @@ -21,6 +21,8 @@ package org.apache.cassandra.locator; import java.io.IOException; import java.util.*; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -28,12 +30,30 @@ import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.apache.cassandra.utils.FBUtilities; import static java.util.concurrent.TimeUnit.MILLISECONDS; public class DynamicEndpointSnitchTest { + private double oldBadness; + + @Before + public void before() + { + oldBadness = DatabaseDescriptor.getDynamicBadnessThreshold(); + DatabaseDescriptor.setDynamicBadnessThreshold(0.1); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); + } + + @After + public void after() + { + DatabaseDescriptor.setDynamicBadnessThreshold(oldBadness); + } @BeforeClass public static void setupDD() diff --git a/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java b/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java index 09e37221d3..ef54a60c43 100644 --- a/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/Ec2SnitchTest.java @@ -19,26 +19,22 @@ package org.apache.cassandra.locator; import java.util.Collections; -import java.util.EnumMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.commitlog.CommitLog; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.mockito.stubbing.Answer; -import static org.apache.cassandra.ServerTestUtils.cleanup; -import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.apache.cassandra.locator.Ec2MultiRegionSnitch.PRIVATE_IP_QUERY; import static org.apache.cassandra.locator.Ec2MultiRegionSnitch.PUBLIC_IP_QUERY; @@ -68,12 +64,7 @@ public class Ec2SnitchTest { GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - CommitLog.instance.segmentManager.awaitManagementTasksCompletion(); - mkdirs(); - cleanup(); - Keyspace.setInitialized(); - StorageService.instance.initServer(0); + ClusterMetadataTestHelper.setInstanceForTest(); } @@ -83,6 +74,12 @@ public class Ec2SnitchTest StorageService.instance.stopClient(); } + @Before + public void resetCMS() + { + ServerTestUtils.resetCMS(); + } + @Test public void testLegacyRac() throws Exception { @@ -257,11 +254,8 @@ public class Ec2SnitchTest InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); - Gossiper.instance.addSavedEndpoint(nonlocal); - Map stateMap = new EnumMap<>(ApplicationState.class); - stateMap.put(ApplicationState.DC, StorageService.instance.valueFactory.datacenter("us-west")); - stateMap.put(ApplicationState.RACK, StorageService.instance.valueFactory.datacenter("1a")); - Gossiper.instance.getEndpointStateForEndpoint(nonlocal).addApplicationStates(stateMap); + Token t1 = ClusterMetadata.current().partitioner.getRandomToken(); + ClusterMetadataTestHelper.addEndpoint(nonlocal, t1, "us-west", "1a"); assertEquals("us-west", snitch.getDatacenter(nonlocal)); assertEquals("1a", snitch.getRack(nonlocal)); diff --git a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java index 9dffbac2d1..b447034e95 100644 --- a/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/GoogleCloudSnitchTest.java @@ -20,26 +20,18 @@ package org.apache.cassandra.locator; import java.io.IOException; -import java.util.EnumMap; -import java.util.Map; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.DefaultCloudMetadataServiceConnector; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.Pair; -import static org.apache.cassandra.ServerTestUtils.cleanup; -import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; import static org.apache.cassandra.locator.AbstractCloudMetadataServiceConnector.METADATA_URL_PROPERTY; import static org.apache.cassandra.locator.AlibabaCloudSnitch.DEFAULT_METADATA_SERVICE_URL; @@ -56,12 +48,7 @@ public class GoogleCloudSnitchTest { GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - CommitLog.instance.segmentManager.awaitManagementTasksCompletion(); - mkdirs(); - cleanup(); - Keyspace.setInitialized(); - StorageService.instance.initServer(0); + ClusterMetadataTestHelper.setInstanceForTest(); } @Test @@ -78,11 +65,8 @@ public class GoogleCloudSnitchTest InetAddressAndPort local = InetAddressAndPort.getByName("127.0.0.1"); InetAddressAndPort nonlocal = InetAddressAndPort.getByName("127.0.0.7"); - Gossiper.instance.addSavedEndpoint(nonlocal); - Map stateMap = new EnumMap<>(ApplicationState.class); - stateMap.put(ApplicationState.DC, StorageService.instance.valueFactory.datacenter("europe-west1")); - stateMap.put(ApplicationState.RACK, StorageService.instance.valueFactory.datacenter("a")); - Gossiper.instance.getEndpointStateForEndpoint(nonlocal).addApplicationStates(stateMap); + Token t1 = ClusterMetadata.current().partitioner.getRandomToken(); + ClusterMetadataTestHelper.addEndpoint(nonlocal, t1, "europe-west1", "a"); assertEquals("europe-west1", snitch.getDatacenter(nonlocal)); assertEquals("a", snitch.getRack(nonlocal)); @@ -106,10 +90,4 @@ public class GoogleCloudSnitchTest assertEquals("asia-east1", snitch.getDatacenter(local)); assertEquals("a", snitch.getRack(local)); } - - @AfterClass - public static void tearDown() - { - StorageService.instance.stopClient(); - } } diff --git a/test/unit/org/apache/cassandra/locator/MetaStrategyTest.java b/test/unit/org/apache/cassandra/locator/MetaStrategyTest.java new file mode 100644 index 0000000000..1e93b87df2 --- /dev/null +++ b/test/unit/org/apache/cassandra/locator/MetaStrategyTest.java @@ -0,0 +1,161 @@ +/* + * 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.locator; + +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.TokenMap; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LockedRanges; + +public class MetaStrategyTest +{ + static + { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + } + + static class NodeConfiguration + { + final NodeAddresses addresses; + final Location location; + final long token; + + NodeConfiguration(NodeAddresses addresses, Location location, long token) + { + this.addresses = addresses; + this.location = location; + this.token = token; + } + } + + public static NodeConfiguration node(NodeAddresses addresses, Location location, long token) + { + return new NodeConfiguration(addresses, location, token); + } + + public static ClusterMetadata metadata(NodeConfiguration... configurations) + { + Directory directory = new Directory(); + TokenMap tokenMap = new TokenMap(Murmur3Partitioner.instance); + for (NodeConfiguration configuration : configurations) + { + directory = directory.with(configuration.addresses, configuration.location); + directory = directory.withRackAndDC(directory.peerId(configuration.addresses.broadcastAddress)); + tokenMap = tokenMap.assignTokens(directory.peerId(configuration.addresses.broadcastAddress), Collections.singleton(new Murmur3Partitioner.LongToken(configuration.token))); + } + + return new ClusterMetadata(Epoch.EMPTY, + Period.EMPTY, + true, + Murmur3Partitioner.instance, + DistributedSchema.empty(), + directory, + tokenMap, + DataPlacements.EMPTY, + LockedRanges.EMPTY, + InProgressSequences.EMPTY, + ImmutableMap.of()); + } + + @Test + public void testDatacenterAware() throws Throwable + { + ClusterMetadata metadata = metadata(node(addr(1), location("dc1", "rack1"), 1), + node(addr(2), location("dc1", "rack1"), 2), + node(addr(3), location("dc1", "rack1"), 3), + node(addr(4), location("dc2", "rack2"), 4), + node(addr(5), location("dc2", "rack2"), 5), + node(addr(6), location("dc2", "rack2"), 6), + node(addr(7), location("dc3", "rack3"), 7), + node(addr(8), location("dc3", "rack3"), 8), + node(addr(9), location("dc3", "rack3"), 9)); + + Map rf = new HashMap<>(); + rf.put("dc1", 2); + rf.put("dc2", 2); + rf.put("dc3", 2); + + CMSPlacementStrategy placementStrategy = new CMSPlacementStrategy.DatacenterAware(rf, (cd, n) -> true); + Assert.assertEquals(nodeIds(metadata.directory, + 1, 2, 4, 5, 7, 8), + placementStrategy.reconfigure(Collections.EMPTY_SET, metadata)); + + Assert.assertEquals(nodeIds(metadata.directory, + 1, 2, 4, 5, 7, 8), + placementStrategy.reconfigure(nodeIds(metadata.directory, 3, 6, 9), metadata)); + + placementStrategy = new CMSPlacementStrategy.DatacenterAware(rf, (cd, n) -> !n.equals(metadata.directory.peerId(addr(2).broadcastAddress)) && + !n.equals(metadata.directory.peerId(addr(2).broadcastAddress))); + Assert.assertEquals(nodeIds(metadata.directory, + 1, 3, 4, 5, 7, 8), + placementStrategy.reconfigure(Collections.EMPTY_SET, metadata)); + + Assert.assertEquals(nodeIds(metadata.directory, + 1, 3, 4, 5, 7, 8), + placementStrategy.reconfigure(nodeIds(metadata.directory, 3, 6, 9), metadata)); + } + + public static Set nodeIds(Directory directory, int... addrs) throws UnknownHostException + { + Set nodeIds = new HashSet<>(); + for (int addr : addrs) + nodeIds.add(directory.peerId(InetAddressAndPort.getByName("127.0.0." + addr))); + return nodeIds; + } + + public static NodeAddresses addr(int i) + { + try + { + InetAddressAndPort inetAddressAndPort = InetAddressAndPort.getByName("127.0.0." + i); + return new NodeAddresses(inetAddressAndPort); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public static Location location(String dc, String rack) + { + return new Location(dc, rack); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java b/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java index 81d6694c72..9a97999827 100644 --- a/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java +++ b/test/unit/org/apache/cassandra/locator/NetworkTopologyStrategyTest.java @@ -26,29 +26,28 @@ import java.util.stream.Collectors; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; -import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.ExpectedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.*; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; import org.apache.cassandra.dht.OrderPreservingPartitioner.StringToken; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.locator.TokenMetadata.Topology; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.compatibility.TokenRingUtils; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.locator.NetworkTopologyStrategy.REPLICATION_FACTOR; @@ -58,14 +57,22 @@ import static org.junit.Assert.assertTrue; public class NetworkTopologyStrategyTest { - private static final String KEYSPACE = "Keyspace1"; + private static final String KEYSPACE = "ks1"; private static final Logger logger = LoggerFactory.getLogger(NetworkTopologyStrategyTest.class); @BeforeClass public static void setupDD() { DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(OrderPreservingPartitioner.instance); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + ClusterMetadataService.setInstance(ClusterMetadataTestHelper.instanceForTest()); + } + + @After + public void teardown() + { + ServerTestUtils.resetCMS(); } @Test @@ -73,24 +80,25 @@ public class NetworkTopologyStrategyTest { IEndpointSnitch snitch = new PropertyFileSnitch(); DatabaseDescriptor.setEndpointSnitch(snitch); - TokenMetadata metadata = new TokenMetadata(); - createDummyTokens(metadata, true); + createDummyTokens(true); - Map configOptions = new HashMap(); - configOptions.put("DC1", "3"); - configOptions.put("DC2", "2"); - configOptions.put("DC3", "1"); + ClusterMetadataTestHelper.createKeyspace("CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION = {" + + " 'class' : 'NetworkTopologyStrategy'," + + " 'DC1': 3," + + " 'DC2': 2," + + " 'DC3': 1" + + " } ;"); + + NetworkTopologyStrategy strategy = (NetworkTopologyStrategy) ClusterMetadata.current().schema.getKeyspaces().getNullable(KEYSPACE).replicationStrategy; + Assert.assertEquals(strategy.getReplicationFactor("DC1").allReplicas, 3); + Assert.assertEquals(strategy.getReplicationFactor("DC2").allReplicas, 2); + Assert.assertEquals(strategy.getReplicationFactor("DC3").allReplicas, 1); - // Set the localhost to the tokenmetadata. Embedded cassandra way? - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, metadata, snitch, configOptions); - assert strategy.getReplicationFactor("DC1").allReplicas == 3; - assert strategy.getReplicationFactor("DC2").allReplicas == 2; - assert strategy.getReplicationFactor("DC3").allReplicas == 1; // Query for the natural hosts - EndpointsForToken replicas = strategy.getNaturalReplicasForToken(new StringToken("123")); - assert 6 == replicas.size(); - assert 6 == replicas.endpoints().size(); // ensure uniqueness - assert 6 == new HashSet<>(replicas.byEndpoint().values()).size(); // ensure uniqueness + VersionedEndpoints.ForToken replicas = ClusterMetadataTestHelper.getNaturalReplicasForToken(KEYSPACE, new StringToken("123")); + Assert.assertEquals(6, replicas.get().size()); + Assert.assertEquals(6, replicas.get().endpoints().size()); // ensure uniqueness + Assert.assertEquals(6, new HashSet<>(replicas.get().byEndpoint().values()).size()); // ensure uniqueness } @Test @@ -98,24 +106,23 @@ public class NetworkTopologyStrategyTest { IEndpointSnitch snitch = new PropertyFileSnitch(); DatabaseDescriptor.setEndpointSnitch(snitch); - TokenMetadata metadata = new TokenMetadata(); - createDummyTokens(metadata, false); + createDummyTokens(false); - Map configOptions = new HashMap(); + Map configOptions = new HashMap<>(); configOptions.put("DC1", "3"); configOptions.put("DC2", "3"); configOptions.put("DC3", "0"); + NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, configOptions); - // Set the localhost to the tokenmetadata. Embedded cassandra way? - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, metadata, snitch, configOptions); - assert strategy.getReplicationFactor("DC1").allReplicas == 3; - assert strategy.getReplicationFactor("DC2").allReplicas == 3; - assert strategy.getReplicationFactor("DC3").allReplicas == 0; + Assert.assertEquals(strategy.getReplicationFactor("DC1").allReplicas, 3); + Assert.assertEquals(strategy.getReplicationFactor("DC2").allReplicas, 3); + Assert.assertEquals(strategy.getReplicationFactor("DC3").allReplicas, 0); // Query for the natural hosts - EndpointsForToken replicas = strategy.getNaturalReplicasForToken(new StringToken("123")); - assert 6 == replicas.size(); - assert 6 == replicas.endpoints().size(); // ensure uniqueness - assert 6 == new HashSet<>(replicas.byEndpoint().values()).size(); // ensure uniqueness + Token token = new StringToken("123"); + EndpointsForToken replicas = strategy.calculateNaturalReplicas(token, ClusterMetadata.current()).forToken(token); + Assert.assertEquals(6, replicas.size()); + Assert.assertEquals(6, replicas.endpoints().size()); // ensure uniqueness + Assert.assertEquals(6, new HashSet<>(replicas.byEndpoint().values()).size()); // ensure uniqueness } @Test @@ -127,7 +134,7 @@ public class NetworkTopologyStrategyTest IEndpointSnitch snitch = new RackInferringSnitch(); DatabaseDescriptor.setEndpointSnitch(snitch); - TokenMetadata metadata = new TokenMetadata(); + Map configOptions = new HashMap(); Multimap tokens = HashMultimap.create(); @@ -145,16 +152,16 @@ public class NetworkTopologyStrategyTest StringToken token = new StringToken(String.format("%02x%02x%02x", ep, rack, dc)); logger.debug("adding node {} at {}", address, token); tokens.put(address, token); + ClusterMetadataTestHelper.addEndpoint(address, token, snitch.getDatacenter(address), snitch.getRack(address)); } } } - metadata.updateNormalTokens(tokens); - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, metadata, snitch, configOptions); + NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, configOptions); for (String testToken : new String[]{"123456", "200000", "000402", "ffffff", "400200"}) { - EndpointsForRange replicas = strategy.calculateNaturalReplicas(new StringToken(testToken), metadata); + EndpointsForRange replicas = strategy.calculateNaturalReplicas(new StringToken(testToken), ClusterMetadata.current()); Set endpointSet = replicas.endpoints(); Assert.assertEquals(totalRF, replicas.size()); @@ -164,33 +171,35 @@ public class NetworkTopologyStrategyTest } } - public void createDummyTokens(TokenMetadata metadata, boolean populateDC3) throws UnknownHostException + public void createDummyTokens(boolean populateDC3) throws UnknownHostException { + Location l1 = new Location("DC1", "Rack1"); + Location l2 = new Location("DC2", "Rack1"); + Location l3 = new Location("DC3", "Rack1"); // DC 1 - tokenFactory(metadata, "123", new byte[]{ 10, 0, 0, 10 }); - tokenFactory(metadata, "234", new byte[]{ 10, 0, 0, 11 }); - tokenFactory(metadata, "345", new byte[]{ 10, 0, 0, 12 }); + tokenFactory("123", new byte[]{ 10, 0, 0, 10 }, l1); + tokenFactory("234", new byte[]{ 10, 0, 0, 11 }, l1); + tokenFactory("345", new byte[]{ 10, 0, 0, 12 }, l1); // Tokens for DC 2 - tokenFactory(metadata, "789", new byte[]{ 10, 20, 114, 10 }); - tokenFactory(metadata, "890", new byte[]{ 10, 20, 114, 11 }); + tokenFactory("789", new byte[]{ 10, 20, 114, 10 }, l2); + tokenFactory("890", new byte[]{ 10, 20, 114, 11 }, l2); //tokens for DC3 if (populateDC3) { - tokenFactory(metadata, "456", new byte[]{ 10, 21, 119, 13 }); - tokenFactory(metadata, "567", new byte[]{ 10, 21, 119, 10 }); + tokenFactory("456", new byte[]{ 10, 21, 119, 13 }, l3); + tokenFactory("567", new byte[]{ 10, 21, 119, 10 }, l3); } // Extra Tokens - tokenFactory(metadata, "90A", new byte[]{ 10, 0, 0, 13 }); + tokenFactory("90A", new byte[]{ 10, 0, 0, 13 }, l1); if (populateDC3) - tokenFactory(metadata, "0AB", new byte[]{ 10, 21, 119, 14 }); - tokenFactory(metadata, "ABC", new byte[]{ 10, 20, 114, 15 }); + tokenFactory("0AB", new byte[]{ 10, 21, 119, 14 }, l3); + tokenFactory("ABC", new byte[]{ 10, 20, 114, 15 }, l2); } - public void tokenFactory(TokenMetadata metadata, String token, byte[] bytes) throws UnknownHostException + public void tokenFactory(String token, byte[] bytes, Location location) throws UnknownHostException { - Token token1 = new StringToken(token); - InetAddressAndPort add1 = InetAddressAndPort.getByAddress(bytes); - metadata.updateNormalToken(token1, add1); + InetAddressAndPort addr = InetAddressAndPort.getByAddress(bytes); + ClusterMetadataTestHelper.addEndpoint(addr, new StringToken(token), location); } @Test @@ -199,35 +208,44 @@ public class NetworkTopologyStrategyTest final int NODES = 100; final int VNODES = 64; final int RUNS = 10; - StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); - Map datacenters = ImmutableMap.of("rf1", 1, "rf3", 3, "rf5_1", 5, "rf5_2", 5, "rf5_3", 5); - List nodes = new ArrayList<>(NODES); - for (byte i=0; i datacenters = ImmutableMap.of("rf1", 1, "rf3", 3, "rf5_1", 5, "rf5_2", 5, "rf5_3", 5); + List nodes = new ArrayList<>(NODES); + for (byte i = 0; i < NODES; ++i) + nodes.add(InetAddressAndPort.getByAddress(new byte[]{ 127, 0, 0, i })); + for (int run = 0; run < RUNS; ++run) + { + ServerTestUtils.resetCMS(); + Random rand = new Random(run); + IEndpointSnitch snitch = generateSnitch(datacenters, nodes, rand); + DatabaseDescriptor.setEndpointSnitch(snitch); - TokenMetadata meta = new TokenMetadata(); - for (int i=0; i tokens = new HashSet<>(); + while (tokens.size() < VNODES) // tokens/vnodes per node + { + tokens.add(Murmur3Partitioner.instance.getRandomToken(rand)); + } + ClusterMetadataTestHelper.addEndpoint(nodes.get(i), tokens, snitch.getDatacenter(nodes.get(i)), snitch.getRack(nodes.get(i))); + } + testEquivalence(ClusterMetadata.current(), snitch, datacenters, rand); + } } } - void testEquivalence(TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map datacenters, Random rand) + void testEquivalence(ClusterMetadata metadata, IEndpointSnitch snitch, Map datacenters, Random rand) { - NetworkTopologyStrategy nts = new NetworkTopologyStrategy("ks", tokenMetadata, snitch, - datacenters.entrySet().stream(). - collect(Collectors.toMap(x -> x.getKey(), x -> Integer.toString(x.getValue())))); + NetworkTopologyStrategy nts = new NetworkTopologyStrategy("ks", + datacenters.entrySet() + .stream() + .collect(Collectors.toMap(x -> x.getKey(), x -> Integer.toString(x.getValue())))); for (int i=0; i<1000; ++i) { Token token = Murmur3Partitioner.instance.getRandomToken(rand); - List expected = calculateNaturalEndpoints(token, tokenMetadata, datacenters, snitch); - List actual = new ArrayList<>(nts.calculateNaturalReplicas(token, tokenMetadata).endpoints()); + List expected = calculateNaturalEndpoints(token, metadata, datacenters, snitch); + List actual = new ArrayList<>(nts.calculateNaturalReplicas(token, metadata).endpoints()); if (endpointsDiffer(expected, actual)) { System.err.println("Endpoints mismatch for token " + token); @@ -300,7 +318,7 @@ public class NetworkTopologyStrategyTest } // Copy of older endpoints calculation algorithm for comparison - public static List calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata, Map datacenters, IEndpointSnitch snitch) + public static List calculateNaturalEndpoints(Token searchToken, ClusterMetadata metadata, Map datacenters, IEndpointSnitch snitch) { // we want to preserve insertion order so that the first added endpoint becomes primary Set replicas = new LinkedHashSet<>(); @@ -309,11 +327,10 @@ public class NetworkTopologyStrategyTest for (Map.Entry dc : datacenters.entrySet()) dcReplicas.put(dc.getKey(), new HashSet(dc.getValue())); - Topology topology = tokenMetadata.getTopology(); // all endpoints in each DC, so we can check when we have exhausted all the members of a DC - Multimap allEndpoints = topology.getDatacenterEndpoints(); + Multimap allEndpoints = metadata.directory.allDatacenterEndpoints(); // all racks in a DC so we can check when we have exhausted all racks in a DC - Map> racks = topology.getDatacenterRacks(); + Map> racks = metadata.directory.allDatacenterRacks(); assert !allEndpoints.isEmpty() && !racks.isEmpty() : "not aware of any cluster members"; // tracks the racks we have already placed replicas in @@ -327,11 +344,11 @@ public class NetworkTopologyStrategyTest for (Map.Entry dc : datacenters.entrySet()) skippedDcEndpoints.put(dc.getKey(), new LinkedHashSet()); - Iterator tokenIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), searchToken, false); + Iterator tokenIter = TokenRingUtils.ringIterator(metadata.tokenMap.tokens(), searchToken, false); while (tokenIter.hasNext() && !hasSufficientReplicas(dcReplicas, allEndpoints, datacenters)) { Token next = tokenIter.next(); - InetAddressAndPort ep = tokenMetadata.getEndpoint(next); + InetAddressAndPort ep = metadata.directory.endpoint(metadata.tokenMap.owner(next)); String dc = snitch.getDatacenter(ep); // have we already found all replicas for this dc? if (!datacenters.containsKey(dc) || hasSufficientReplicas(dc, dcReplicas, allEndpoints, datacenters)) @@ -370,7 +387,7 @@ public class NetworkTopologyStrategyTest } } - return new ArrayList(replicas); + return new ArrayList<>(replicas); } private static boolean hasSufficientReplicas(String dc, Map> dcReplicas, Multimap allEndpoints, Map datacenters) @@ -405,37 +422,36 @@ public class NetworkTopologyStrategyTest @Test public void testTransientReplica() throws Exception { - IEndpointSnitch snitch = new SimpleSnitch(); - DatabaseDescriptor.setEndpointSnitch(snitch); + try (WithPartitioner m3p = new WithPartitioner(Murmur3Partitioner.instance)) + { + IEndpointSnitch snitch = new SimpleSnitch(); + DatabaseDescriptor.setEndpointSnitch(snitch); - List endpoints = Lists.newArrayList(InetAddressAndPort.getByName("127.0.0.1"), - InetAddressAndPort.getByName("127.0.0.2"), - InetAddressAndPort.getByName("127.0.0.3"), - InetAddressAndPort.getByName("127.0.0.4")); + List endpoints = Lists.newArrayList(InetAddressAndPort.getByName("127.0.0.1"), + InetAddressAndPort.getByName("127.0.0.2"), + InetAddressAndPort.getByName("127.0.0.3"), + InetAddressAndPort.getByName("127.0.0.4")); - Multimap tokens = HashMultimap.create(); - tokens.put(endpoints.get(0), tk(100)); - tokens.put(endpoints.get(1), tk(200)); - tokens.put(endpoints.get(2), tk(300)); - tokens.put(endpoints.get(3), tk(400)); - TokenMetadata metadata = new TokenMetadata(); - metadata.updateNormalTokens(tokens); + ClusterMetadataTestHelper.addEndpoint(endpoints.get(0), tk(100), SimpleSnitch.DATA_CENTER_NAME, SimpleSnitch.RACK_NAME); + ClusterMetadataTestHelper.addEndpoint(endpoints.get(1), tk(200), SimpleSnitch.DATA_CENTER_NAME, SimpleSnitch.RACK_NAME); + ClusterMetadataTestHelper.addEndpoint(endpoints.get(2), tk(300), SimpleSnitch.DATA_CENTER_NAME, SimpleSnitch.RACK_NAME); + ClusterMetadataTestHelper.addEndpoint(endpoints.get(3), tk(400), SimpleSnitch.DATA_CENTER_NAME, SimpleSnitch.RACK_NAME); - Map configOptions = new HashMap(); - configOptions.put(snitch.getDatacenter((InetAddressAndPort) null), "3/1"); + Map configOptions = new HashMap<>(); + configOptions.put(SimpleSnitch.DATA_CENTER_NAME, "3/1"); + NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, configOptions); - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy(KEYSPACE, metadata, snitch, configOptions); - - Util.assertRCEquals(EndpointsForRange.of(fullReplica(endpoints.get(0), range(400, 100)), - fullReplica(endpoints.get(1), range(400, 100)), - transientReplica(endpoints.get(2), range(400, 100))), - strategy.getNaturalReplicasForToken(tk(99))); + Util.assertRCEquals(EndpointsForRange.of(fullReplica(endpoints.get(0), range(400, 100)), + fullReplica(endpoints.get(1), range(400, 100)), + transientReplica(endpoints.get(2), range(400, 100))), + strategy.calculateNaturalReplicas(tk(99), ClusterMetadata.current())); - Util.assertRCEquals(EndpointsForRange.of(fullReplica(endpoints.get(1), range(100, 200)), - fullReplica(endpoints.get(2), range(100, 200)), - transientReplica(endpoints.get(3), range(100, 200))), - strategy.getNaturalReplicasForToken(tk(101))); + Util.assertRCEquals(EndpointsForRange.of(fullReplica(endpoints.get(1), range(100, 200)), + fullReplica(endpoints.get(2), range(100, 200)), + transientReplica(endpoints.get(3), range(100, 200))), + strategy.calculateNaturalReplicas(tk(101), ClusterMetadata.current())); + } } @Rule @@ -447,14 +463,11 @@ public class NetworkTopologyStrategyTest expectedEx.expect(ConfigurationException.class); expectedEx.expectMessage(REPLICATION_FACTOR + " should not appear"); - IEndpointSnitch snitch = new SimpleSnitch(); - TokenMetadata metadata = new TokenMetadata(); - Map configOptions = new HashMap<>(); configOptions.put(REPLICATION_FACTOR, "1"); @SuppressWarnings("unused") - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy("ks", metadata, snitch, configOptions); + NetworkTopologyStrategy strategy = new NetworkTopologyStrategy("ks", configOptions); } @Test @@ -462,24 +475,8 @@ public class NetworkTopologyStrategyTest { HashMap configOptions = new HashMap<>(); configOptions.put("DC1", "2"); - - IEndpointSnitch snitch = new AbstractNetworkTopologySnitch() - { - public String getRack(InetAddressAndPort endpoint) - { - return "rack1"; - } - - public String getDatacenter(InetAddressAndPort endpoint) - { - return "DC1"; - } - }; - - NetworkTopologyStrategy strategy = new NetworkTopologyStrategy("ks", new TokenMetadata(), snitch, configOptions); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), FBUtilities.getBroadcastAddressAndPort()); - StorageService.instance.getTokenMetadata().updateNormalToken(new LongToken(1), FBUtilities.getBroadcastAddressAndPort()); - + NetworkTopologyStrategy strategy = new NetworkTopologyStrategy("ks", configOptions); + ClusterMetadataTestHelper.addEndpoint(FBUtilities.getBroadcastAddressAndPort(), new StringToken("123"), "DC1", "RACK1"); ClientWarn.instance.captureWarnings(); strategy.maybeWarnOnOptions(null); assertTrue(ClientWarn.instance.getWarnings().stream().anyMatch(s -> s.contains("Your replication factor"))); diff --git a/test/unit/org/apache/cassandra/locator/PendingRangeMapsTest.java b/test/unit/org/apache/cassandra/locator/PendingRangeMapsTest.java deleted file mode 100644 index 8e0bc00833..0000000000 --- a/test/unit/org/apache/cassandra/locator/PendingRangeMapsTest.java +++ /dev/null @@ -1,108 +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.locator; - -import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.junit.Test; - -import java.net.UnknownHostException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class PendingRangeMapsTest { - - private Range genRange(String left, String right) - { - return new Range(new BigIntegerToken(left), new BigIntegerToken(right)); - } - - private static void addPendingRange(PendingRangeMaps pendingRangeMaps, Range range, String endpoint) - { - try - { - pendingRangeMaps.addPendingRange(range, Replica.fullReplica(InetAddressAndPort.getByName(endpoint), range)); - } - catch (UnknownHostException e) - { - throw new AssertionError(e); - } - } - - @Test - public void testPendingEndpoints() throws UnknownHostException - { - PendingRangeMaps pendingRangeMaps = new PendingRangeMaps(); - - addPendingRange(pendingRangeMaps, genRange("5", "15"), "127.0.0.1"); - addPendingRange(pendingRangeMaps, genRange("15", "25"), "127.0.0.2"); - addPendingRange(pendingRangeMaps, genRange("25", "35"), "127.0.0.3"); - addPendingRange(pendingRangeMaps, genRange("35", "45"), "127.0.0.4"); - addPendingRange(pendingRangeMaps, genRange("45", "55"), "127.0.0.5"); - addPendingRange(pendingRangeMaps, genRange("45", "65"), "127.0.0.6"); - - assertEquals(0, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("0")).size()); - assertEquals(0, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("5")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("10")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("15")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("20")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("25")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("35")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("45")).size()); - assertEquals(2, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("55")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("65")).size()); - - EndpointsForToken replicas = pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("15")); - assertTrue(replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.1"))); - } - - @Test - public void testWrapAroundRanges() throws UnknownHostException - { - PendingRangeMaps pendingRangeMaps = new PendingRangeMaps(); - - addPendingRange(pendingRangeMaps, genRange("5", "15"), "127.0.0.1"); - addPendingRange(pendingRangeMaps, genRange("15", "25"), "127.0.0.2"); - addPendingRange(pendingRangeMaps, genRange("25", "35"), "127.0.0.3"); - addPendingRange(pendingRangeMaps, genRange("35", "45"), "127.0.0.4"); - addPendingRange(pendingRangeMaps, genRange("45", "55"), "127.0.0.5"); - addPendingRange(pendingRangeMaps, genRange("45", "65"), "127.0.0.6"); - addPendingRange(pendingRangeMaps, genRange("65", "7"), "127.0.0.7"); - - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("0")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("5")).size()); - assertEquals(2, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("7")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("10")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("15")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("20")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("25")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("35")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("45")).size()); - assertEquals(2, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("55")).size()); - assertEquals(1, pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("65")).size()); - - EndpointsForToken replicas = pendingRangeMaps.pendingEndpointsFor(new BigIntegerToken("6")); - assertTrue(replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.1"))); - assertTrue(replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.7"))); - } -} diff --git a/test/unit/org/apache/cassandra/locator/PendingRangesTest.java b/test/unit/org/apache/cassandra/locator/PendingRangesTest.java index 992c2dd8e0..15b82a686a 100644 --- a/test/unit/org/apache/cassandra/locator/PendingRangesTest.java +++ b/test/unit/org/apache/cassandra/locator/PendingRangesTest.java @@ -19,25 +19,33 @@ package org.apache.cassandra.locator; import java.net.UnknownHostException; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; -import com.google.common.collect.*; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.quicktheories.core.Gen; -import org.quicktheories.generators.Generate; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.ownership.VersionedEndpoints; +import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.quicktheories.QuickTheory.qt; -import static org.quicktheories.generators.SourceDSL.integers; +@Ignore("TODO: old pending ranges don't exist anymore, consider if we should rewrite these tests or just drop them") public class PendingRangesTest { private static final String RACK1 = "RACK1"; @@ -64,8 +72,15 @@ public class PendingRangesTest public static void beforeClass() throws Throwable { DatabaseDescriptor.daemonInitialization(); - IEndpointSnitch snitch = snitch(); - DatabaseDescriptor.setEndpointSnitch(snitch); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + } + + @Before + public void setup() + { + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(ClusterMetadataTestHelper.syncInstanceForTest()); + ClusterMetadataService.instance().log().bootstrap(FBUtilities.getBroadcastAddressAndPort()); } @Test @@ -133,37 +148,37 @@ public class PendingRangesTest * so node4A incorrectly becomes a pending endpoint for an additional sub-range: (50, 0). * */ - TokenMetadata tm = new TokenMetadata(); - AbstractReplicationStrategy replicationStrategy = simpleStrategy(tm, 3); // setup initial ring - addNode(tm, PEER1, TOKEN1); - addNode(tm, PEER2, TOKEN2); - addNode(tm, PEER3, TOKEN3); - addNode(tm, PEER4, TOKEN4); - addNode(tm, PEER5, TOKEN5); - addNode(tm, PEER6, TOKEN6); + addNode(PEER1, TOKEN1); + addNode(PEER2, TOKEN2); + addNode(PEER3, TOKEN3); + addNode(PEER4, TOKEN4); + addNode(PEER5, TOKEN5); + addNode(PEER6, TOKEN6); + + AbstractReplicationStrategy replicationStrategy = simpleStrategy(3); + ClusterMetadataTestHelper.createKeyspace(KEYSPACE, KeyspaceParams.simple(3)); + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ks = metadata.schema.getKeyspaceMetadata(KEYSPACE); // no pending ranges before any replacements - tm.calculatePendingRanges(replicationStrategy, KEYSPACE); - assertEquals(0, Iterators.size(tm.getPendingRanges(KEYSPACE).iterator())); + assertEquals(0, metadata.pendingRanges(ks).size()); // Ranges initially owned by PEER1 and PEER4 - RangesAtEndpoint peer1Ranges = replicationStrategy.getAddressReplicas(tm).get(PEER1); - RangesAtEndpoint peer4Ranges = replicationStrategy.getAddressReplicas(tm).get(PEER4); + RangesAtEndpoint peer1Ranges = replicationStrategy.getAddressReplicas(metadata).get(PEER1).unwrap(); + RangesAtEndpoint peer4Ranges = replicationStrategy.getAddressReplicas(metadata).get(PEER4).unwrap(); // Replace PEER1 with PEER1A - replace(PEER1, PEER1A, TOKEN1, tm, replicationStrategy); + replace(PEER1, PEER1A, TOKEN1); // The only pending ranges should be the ones previously belonging to PEER1 // and these should have a single pending endpoint, PEER1A RangesByEndpoint.Builder b1 = new RangesByEndpoint.Builder(); peer1Ranges.iterator().forEachRemaining(replica -> b1.put(PEER1A, new Replica(PEER1A, replica.range(), replica.isFull()))); RangesByEndpoint expected = b1.build(); - assertPendingRanges(tm.getPendingRanges(KEYSPACE), expected); - // Also verify the Multimap variant of getPendingRanges - assertPendingRanges(tm.getPendingRangesMM(KEYSPACE), expected); + assertPendingRanges(ClusterMetadata.current().pendingRanges(ks), expected); // Replace PEER4 with PEER4A - replace(PEER4, PEER4A, TOKEN4, tm, replicationStrategy); + replace(PEER4, PEER4A, TOKEN4); // Pending ranges should now include the ranges originally belonging // to PEER1 (now pending for PEER1A) and the ranges originally belonging to PEER4 // (now pending for PEER4A). @@ -171,16 +186,16 @@ public class PendingRangesTest peer1Ranges.iterator().forEachRemaining(replica -> b2.put(PEER1A, new Replica(PEER1A, replica.range(), replica.isFull()))); peer4Ranges.iterator().forEachRemaining(replica -> b2.put(PEER4A, new Replica(PEER4A, replica.range(), replica.isFull()))); expected = b2.build(); - assertPendingRanges(tm.getPendingRanges(KEYSPACE), expected); - assertPendingRanges(tm.getPendingRangesMM(KEYSPACE), expected); + assertPendingRanges(ClusterMetadata.current().pendingRanges(ks), expected); } @Test public void testConcurrentAdjacentLeaveAndMove() { - TokenMetadata tm = new TokenMetadata(); - AbstractReplicationStrategy strategy = simpleStrategy(tm, 3); - + // TODO This fails with TCM as the constraints regarding concurrent overlapping range movements and + // replication factors have moved into the operations themselves. Previously, these were enforced + // at the StorageService level. + ClusterMetadataTestHelper.createKeyspace(KEYSPACE, KeyspaceParams.simple(3)); Token newToken = token(0); Token token1 = token(-9); Token token2 = token(-5); @@ -195,32 +210,33 @@ public class PendingRangesTest InetAddressAndPort node5 = peer(5); // setup initial ring - addNode(tm, node1, token1); - addNode(tm, node2, token2); - addNode(tm, node3, token3); - addNode(tm, node4, token4); - addNode(tm, node5, token5); + addNode(node1, token1); + addNode(node2, token2); + addNode(node3, token3); + addNode(node4, token4); + addNode(node5, token5); - tm.addLeavingEndpoint(node5); - tm.addMovingEndpoint(newToken, node3); - - tm.calculatePendingRanges(strategy, KEYSPACE); + leave(node5); + move(node3, newToken); + Map pendingByEndpoint = getPendingRangesByEndpoint(); assertRangesAtEndpoint(RangesAtEndpoint.of(new Replica(node1, new Range<>(token2, token3), true)), - tm.getPendingRanges(KEYSPACE, node1)); + pendingByEndpoint.get(node1)); assertRangesAtEndpoint(RangesAtEndpoint.of(new Replica(node2, new Range<>(token3, token4), true)), - tm.getPendingRanges(KEYSPACE, node2)); + pendingByEndpoint.get(node2)); assertRangesAtEndpoint(RangesAtEndpoint.of(new Replica(node3, new Range<>(token3, newToken), true), new Replica(node3, new Range<>(token4, token5), true)), - tm.getPendingRanges(KEYSPACE, node3)); - assertRangesAtEndpoint(RangesAtEndpoint.empty(node4), tm.getPendingRanges(KEYSPACE, node4)); - assertRangesAtEndpoint(RangesAtEndpoint.empty(node5), tm.getPendingRanges(KEYSPACE, node5)); + pendingByEndpoint.get(node3)); + assertRangesAtEndpoint(RangesAtEndpoint.empty(node4), pendingByEndpoint.get(node4)); + assertRangesAtEndpoint(RangesAtEndpoint.empty(node5), pendingByEndpoint.get(node5)); } @Test public void testConcurrentAdjacentLeavingNodes() { - TokenMetadata tm = new TokenMetadata(); - AbstractReplicationStrategy strategy = simpleStrategy(tm, 2); + // TODO This fails with TCM as the constraints regarding concurrent overlapping range movements and + // replication factors have moved into the operations themselves. Previously, these were enforced + // at the StorageService level. + ClusterMetadataTestHelper.createKeyspace(KEYSPACE, KeyspaceParams.simple(2)); Token token1 = token(-9); Token token2 = token(-4); @@ -232,24 +248,26 @@ public class PendingRangesTest InetAddressAndPort node3 = peer(3); InetAddressAndPort node4 = peer(4); - addNode(tm, node1, token1); - addNode(tm, node2, token2); - addNode(tm, node3, token3); - addNode(tm, node4, token4); + addNode(node1, token1); + addNode(node2, token2); + addNode(node3, token3); + addNode(node4, token4); - tm.addLeavingEndpoint(node2); - tm.addLeavingEndpoint(node3); - - tm.calculatePendingRanges(strategy, KEYSPACE); + leave(node2); + leave(node3); + Map pendingByEndpoint = getPendingRangesByEndpoint(); assertRangesAtEndpoint(RangesAtEndpoint.of(new Replica(node1, new Range<>(token1, token3), true)), - tm.getPendingRanges(KEYSPACE, node1)); - assertRangesAtEndpoint(RangesAtEndpoint.empty(node2), tm.getPendingRanges(KEYSPACE, node2)); - assertRangesAtEndpoint(RangesAtEndpoint.empty(node3), tm.getPendingRanges(KEYSPACE, node3)); + pendingByEndpoint.get(node1)); + assertRangesAtEndpoint(RangesAtEndpoint.empty(node2), pendingByEndpoint.get(node2)); + assertRangesAtEndpoint(RangesAtEndpoint.empty(node3), pendingByEndpoint.get(node3)); assertRangesAtEndpoint(RangesAtEndpoint.of(new Replica(node4, new Range<>(token4, token1), true), new Replica(node4, new Range<>(token1, token2), true)), - tm.getPendingRanges(KEYSPACE, node4)); + pendingByEndpoint.get(node4)); } + // TODO assess whether it's valuable to port these randomised tests to TCM given that we have a largely equivalent + // test in MetadataChangeSimulationTest +/* @Test public void testBootstrapLeaveAndMovePermutationsWithoutVnodes() { @@ -356,7 +374,7 @@ public class PendingRangesTest private static class Cluster { - private final TokenMetadata tm; + private TokenMetadata tm; private final int tokensPerNode; private final AbstractReplicationStrategy strategy; @@ -365,43 +383,57 @@ public class PendingRangesTest Cluster(int rf, int initialNodes, int tokensPerNode) { - this.tm = new TokenMetadata(); - this.tokensPerNode = tokensPerNode; - this.strategy = simpleStrategy(tm, rf); - this.nodes = new ArrayList<>(initialNodes); - for (int i = 0; i < initialNodes; i++) - addInitialNode(); + + this.tokensPerNode = tokensPerNode; + this.tm = buildTmd(initialNodes); + + this.strategy = simpleStrategy(rf); } - private void addInitialNode() + private TokenMetadata buildTmd(int initialNodes) { - InetAddressAndPort node = peer(nodes.size() + 1); - tm.updateHostId(UUID.randomUUID(), node); - tm.updateNormalTokens(tokens(), node); - nodes.add(node); + TokenMetadata tmd = new DefaultTokenMetadata(); + TokenMetadata.Transformer tmdTransformer = tmd.transformer(); + + for (int i = 0; i < initialNodes; i++) + { + int id = nodes.size() + 1; + InetAddressAndPort node = peer(id); + tmdTransformer = tmdTransformer.withHostId(new UUID(0, id), node) + .withNormalTokens(tokens(), node); + nodes.add(node); + } + return tmdTransformer.build(); } private void bootstrapNode() { - InetAddressAndPort node = peer(nodes.size() + 1); - tm.updateHostId(UUID.randomUUID(), node); - tm.addBootstrapTokens(tokens(), node); + int id = nodes.size() + 1; + InetAddressAndPort node = peer(id); + TokenMetadata.Transformer transformer = tm.transformer() + .withHostId(new UUID(0, id), node); + transformer.withBootstrapTokens(tokens(), node, null); nodes.add(node); + tm = transformer.build(); } void calculateAndGetPendingRanges() { // test that it does not crash - tm.calculatePendingRanges(strategy, KEYSPACE); for (InetAddressAndPort node : nodes) - tm.getPendingRanges(KEYSPACE, node); + { + ((DefaultTokenMetadata) tm).getPendingRanges(KEYSPACE, strategy).atEndpoint(node); + } } Cluster decommissionNodes(int cnt) { + TokenMetadata.Transformer transformer = tm.transformer(); for (int i = 0; i < cnt; i++) - tm.addLeavingEndpoint(nodes.get(random.nextInt(nodes.size()))); + transformer = transformer.withLeavingEndpoint(nodes.get(random.nextInt(nodes.size()))); + + tm = transformer.build(); return this; } @@ -421,17 +453,22 @@ public class PendingRangesTest return this; } - private void moveNode() + synchronized private void moveNode() { - if (tm.getSizeOfMovingEndpoints() >= nodes.size()) + if (tm.getMovingEndpoints().size() >= nodes.size()) throw new IllegalStateException("Number of movements should not exceed total nodes in the cluster"); // we want to ensure that any given node is only marked as moving once. + // todo: made sure we don't try to move a leaving node - this seems to have been allowed before, investigate why List moveCandidates = nodes.stream() - .filter(node -> !tm.isMoving(node)) + .filter(node -> tm.getMovingEndpoints().stream().noneMatch(p -> p.right.equals(node))) + .filter(node -> tm.getLeavingEndpoints().stream().noneMatch(node::equals)) .collect(Collectors.toList()); - InetAddressAndPort node = moveCandidates.get(random.nextInt(moveCandidates.size())); - tm.addMovingEndpoint(token(random.nextLong()), node); + if (moveCandidates.size() > 0) + { + InetAddressAndPort node = moveCandidates.get(random.nextInt(moveCandidates.size())); + tm = tm.transformer().withMovingEndpoint(token(random.nextLong()), node).build(); + } } private Collection tokens() @@ -451,12 +488,30 @@ public class PendingRangesTest tm.toString()); } } +*/ - private void assertPendingRanges(PendingRangeMaps pending, RangesByEndpoint expected) + private static Map getPendingRangesByEndpoint() + { + ClusterMetadata metadata = ClusterMetadata.current(); + KeyspaceMetadata ks = metadata.schema.getKeyspaceMetadata(KEYSPACE); + Map, VersionedEndpoints.ForRange> pending = metadata.pendingRanges(ks); + Map byEndpointBuilder = new HashMap<>(); + pending.forEach((range, endpoints) -> { + endpoints.forEach(r -> { + RangesAtEndpoint.Builder builder = byEndpointBuilder.computeIfAbsent(r.endpoint(), RangesAtEndpoint::builder); + builder.add(r); + }); + }); + Map pendingByEndpoint = new HashMap<>(); + byEndpointBuilder.forEach((endpoint, builder) -> pendingByEndpoint.put(endpoint, builder.build())); + return pendingByEndpoint; + } + + private void assertPendingRanges(Map, VersionedEndpoints.ForRange> pending, RangesByEndpoint expected) { RangesByEndpoint.Builder actual = new RangesByEndpoint.Builder(); - pending.iterator().forEachRemaining(pendingRange -> { - Replica replica = Iterators.getOnlyElement(pendingRange.getValue().iterator()); + pending.entrySet().forEach(pendingRange -> { + Replica replica = Iterators.getOnlyElement(pendingRange.getValue().get().iterator()); actual.put(replica.endpoint(), replica); }); assertRangesByEndpoint(expected, actual.build()); @@ -471,8 +526,8 @@ public class PendingRangesTest private void assertRangesAtEndpoint(RangesAtEndpoint expected, RangesAtEndpoint actual) { - assertEquals(expected.size(), actual.size()); - assertTrue(Iterables.all(expected, actual::contains)); + assertEquals("expected = "+expected +", actual = " + actual, expected.size(), actual.size()); + assertTrue("expected = "+expected +", actual = " + actual , Iterables.all(expected, actual::contains)); } private void assertRangesByEndpoint(RangesByEndpoint expected, RangesByEndpoint actual) @@ -486,20 +541,44 @@ public class PendingRangesTest } } - private void addNode(TokenMetadata tm, InetAddressAndPort replica, Token token) + private void addNode(InetAddressAndPort replica, Token token) { - tm.updateNormalTokens(Collections.singleton(token), replica); + registerNode(replica); + ClusterMetadataTestHelper.join(replica, Collections.singleton(token)); + } + + private void registerNode(InetAddressAndPort replica) + { + ClusterMetadataTestHelper.register(replica, DC1, RACK1); } private void replace(InetAddressAndPort toReplace, InetAddressAndPort replacement, - Token token, - TokenMetadata tm, - AbstractReplicationStrategy replicationStrategy) + Token token) { - assertEquals(toReplace, tm.getEndpoint(token)); - tm.addReplaceTokens(Collections.singleton(token), replacement, toReplace); - tm.calculatePendingRanges(replicationStrategy, KEYSPACE); + // begin replacement, but don't complete it + ClusterMetadata metadata = ClusterMetadata.current(); + assertEquals(toReplace, metadata.directory.endpoint(metadata.tokenMap.owner(token))); + registerNode(replacement); + ClusterMetadataTestHelper.lazyReplace(toReplace, replacement) + .prepareReplace() + .startReplace(); + } + + private void leave(InetAddressAndPort leaving) + { + // begin a decommission, but don't complete it + ClusterMetadataTestHelper.lazyLeave(leaving, false) + .prepareLeave() + .startLeave(); + } + + private void move(InetAddressAndPort moving, Token newToken) + { + // begin a move, but don't complete it + ClusterMetadataTestHelper.lazyMove(moving, Collections.singleton(newToken)) + .prepareMove() + .startMove(); } private static Token token(long token) @@ -535,11 +614,9 @@ public class PendingRangesTest }; } - private static AbstractReplicationStrategy simpleStrategy(TokenMetadata tokenMetadata, int replicationFactor) + private static AbstractReplicationStrategy simpleStrategy(int replicationFactor) { return new SimpleStrategy(KEYSPACE, - tokenMetadata, - DatabaseDescriptor.getEndpointSnitch(), Collections.singletonMap("replication_factor", Integer.toString(replicationFactor))); } } diff --git a/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java b/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java index af9e9704cf..d05c7efea0 100644 --- a/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java +++ b/test/unit/org/apache/cassandra/locator/PropertyFileSnitchTest.java @@ -18,39 +18,34 @@ package org.apache.cassandra.locator; import java.io.IOException; -import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.Random; import java.util.regex.Matcher; +import java.util.stream.Collectors; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.RandomPartitioner; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.FBUtilities; - +import com.google.common.collect.ImmutableMap; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; -import static org.junit.Assert.*; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; +import org.apache.cassandra.tcm.membership.MembershipUtils; +import org.apache.cassandra.utils.FBUtilities; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Unit tests for {@link PropertyFileSnitch}. @@ -59,9 +54,7 @@ public class PropertyFileSnitchTest { private Path effectiveFile; private Path backupFile; - - private VersionedValue.VersionedValueFactory valueFactory; - private Map> tokenMap; + private InetAddressAndPort localAddress; @BeforeClass public static void setupDD() @@ -72,32 +65,73 @@ public class PropertyFileSnitchTest @Before public void setup() throws ConfigurationException, IOException { - GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); String confFile = FBUtilities.resourceToFile(PropertyFileSnitch.SNITCH_PROPERTIES_FILENAME); effectiveFile = Paths.get(confFile); backupFile = Paths.get(confFile + ".bak"); + localAddress = FBUtilities.getBroadcastAddressAndPort(); restoreOrigConfigFile(); + } - InetAddressAndPort[] hosts = { - InetAddressAndPort.getByName("127.0.0.1"), // this exists in the config file - InetAddressAndPort.getByName("127.0.0.2"), // this exists in the config file - InetAddressAndPort.getByName("127.0.0.9"), // this does not exist in the config file - }; + @Test + public void localLocationPresentInConfig() throws IOException + { + replaceConfigFile(Collections.singletonMap(localAddress.getHostAddressAndPort(), "DC1:RAC2")); + PropertyFileSnitch snitch = new PropertyFileSnitch(); + assertEquals("DC1", snitch.getDatacenter(localAddress)); + assertEquals("RAC2", snitch.getRack(localAddress)); + } - IPartitioner partitioner = new RandomPartitioner(); - valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - tokenMap = new HashMap<>(); + @Test + public void localLocationNotPresentInConfig() throws IOException + { + replaceConfigFile(Collections.singletonMap("default", "DEFAULT_DC:DEFAULT_RACK")); + PropertyFileSnitch snitch = new PropertyFileSnitch(); + assertEquals("DEFAULT_DC", snitch.getDatacenter(localAddress)); + assertEquals("DEFAULT_RACK", snitch.getRack(localAddress)); + } - for (InetAddressAndPort host : hosts) + @Test + public void localAndDefaultLocationNotPresentInConfig() throws IOException + { + replaceConfigFile(Collections.emptyMap()); + try { - Set tokens = Collections.singleton(partitioner.getRandomToken()); - Gossiper.instance.initializeNodeUnsafe(host, UUID.randomUUID(), 1); - Gossiper.instance.injectApplicationState(host, ApplicationState.TOKENS, valueFactory.tokens(tokens)); - - setNodeShutdown(host); - tokenMap.put(host, tokens); + PropertyFileSnitch snitch = new PropertyFileSnitch(); + fail("Expected ConfigurationException"); } + catch (ConfigurationException e) + { + String expectedMessage = String.format("Snitch definitions at %s do not define a location for this node's " + + "broadcast address %s, nor does it provides a default", + PropertyFileSnitch.SNITCH_PROPERTIES_FILENAME, localAddress); + assertTrue(e.getMessage().contains(expectedMessage)); + } + } + + @Test + public void configContainsRemoteConfig() throws IOException + { + // Locations of remote peers should not be accessible from this snitch unless + // they are present in ClusterMetadata + Random r = new Random(System.nanoTime()); + InetAddressAndPort peer = MembershipUtils.randomEndpoint(r); + replaceConfigFile(ImmutableMap.of(localAddress.getHostAddressAndPort(), "DC1:RAC1", + peer.getHostAddressAndPort(), "OTHER_DC1:OTHER_RAC1")); + PropertyFileSnitch snitch = new PropertyFileSnitch(); + assertEquals("DC1", snitch.getDatacenter(localAddress)); + assertEquals("RAC1", snitch.getRack(localAddress)); + + assertEquals(PropertyFileSnitch.DEFAULT_DC, snitch.getDatacenter(peer)); + assertEquals(PropertyFileSnitch.DEFAULT_RACK, snitch.getRack(peer)); + + // Register peer, causing ClusterMetadata to be updated. Note that the location + // here is not the one in the config file, that should still be irrelevant + ClusterMetadataTestHelper.register(peer, "OTHER_DC2", "OTHER_RAC2"); + assertEquals("OTHER_DC2", snitch.getDatacenter(peer)); + assertEquals("OTHER_RAC2", snitch.getRack(peer)); } private void restoreOrigConfigFile() throws IOException @@ -111,246 +145,16 @@ public class PropertyFileSnitchTest private void replaceConfigFile(Map replacements) throws IOException { - List lines = Files.readAllLines(effectiveFile, StandardCharsets.UTF_8); - List newLines = new ArrayList<>(lines.size()); - Set replaced = new HashSet<>(); - - for (String line : lines) - { - String[] info = line.split("="); - if (info.length == 2 && !line.startsWith("#") && !line.startsWith("default=")) - { - InetAddressAndPort address = InetAddressAndPort.getByName(info[0].replaceAll(Matcher.quoteReplacement("\\:"), ":")); - String replacement = replacements.get(address.getHostAddressAndPort()); - if (replacement != null) - { - if (!replacement.isEmpty()) // empty means remove this line - newLines.add(info[0] + '=' + replacement); - - replaced.add(address.getHostAddressAndPort()); - } - else - { - newLines.add(line); - } - } - else - { - newLines.add(line); - } - } - - // add any new lines that were not replaced - for (Map.Entry replacement : replacements.entrySet()) - { - if (replaced.contains(replacement.getKey())) - continue; - - if (!replacement.getValue().isEmpty()) // empty means remove this line so do nothing here - { - String escaped = replacement.getKey().replaceAll(Matcher.quoteReplacement(":"), "\\\\:"); - newLines.add(escaped + '=' + replacement.getValue()); - } - } - + List newLines = replacements.entrySet() + .stream() + .map(e -> toLine(e.getKey(), e.getValue())) + .collect(Collectors.toList()); Files.write(effectiveFile, newLines, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING); } - private void setNodeShutdown(InetAddressAndPort host) + private String toLine(String key, String value) { - StorageService.instance.getTokenMetadata().removeEndpoint(host); - Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS_WITH_PORT, valueFactory.shutdown(true)); - Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS, valueFactory.shutdown(true)); - Gossiper.instance.markDead(host, Gossiper.instance.getEndpointStateForEndpoint(host)); - } - - private void setNodeLive(InetAddressAndPort host) - { - Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS_WITH_PORT, valueFactory.normal(tokenMap.get(host))); - Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS, valueFactory.normal(tokenMap.get(host))); - Gossiper.instance.realMarkAlive(host, Gossiper.instance.getEndpointStateForEndpoint(host)); - StorageService.instance.getTokenMetadata().updateNormalTokens(tokenMap.get(host), host); - } - - private static void checkEndpoint(final AbstractNetworkTopologySnitch snitch, - final String endpointString, final String expectedDatacenter, - final String expectedRack) throws UnknownHostException - { - final InetAddressAndPort endpoint = InetAddressAndPort.getByName(endpointString); - assertEquals(expectedDatacenter, snitch.getDatacenter(endpoint)); - assertEquals(expectedRack, snitch.getRack(endpoint)); - } - - /** - * Test that changing rack for a host in the configuration file is only effective if the host is not live. - * The original configuration file contains: 127.0.0.1=DC1:RAC1 - */ - @Test - public void testChangeHostRack() throws Exception - { - final InetAddressAndPort host = InetAddressAndPort.getByName("127.0.0.1"); - final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC1"); - - try - { - setNodeLive(host); - - Files.copy(effectiveFile, backupFile); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC1:RAC2")); - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC1"); - - setNodeShutdown(host); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC1:RAC2")); - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC2"); - } - finally - { - restoreOrigConfigFile(); - setNodeShutdown(host); - } - } - - /** - * Test that changing dc for a host in the configuration file is only effective if the host is not live. - * The original configuration file contains: 127.0.0.1=DC1:RAC1 - */ - @Test - public void testChangeHostDc() throws Exception - { - final InetAddressAndPort host = InetAddressAndPort.getByName("127.0.0.1"); - final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC1"); - - try - { - setNodeLive(host); - - Files.copy(effectiveFile, backupFile); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC2:RAC1")); - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC1"); - - setNodeShutdown(host); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC2:RAC1")); - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC2", "RAC1"); - } - finally - { - restoreOrigConfigFile(); - setNodeShutdown(host); - } - } - - /** - * Test that adding a host to the configuration file changes the host dc and rack only if the host - * is not live. The original configuration file does not contain 127.0.0.9 and so it should use - * the default default=DC1:r1. - */ - @Test - public void testAddHost() throws Exception - { - final InetAddressAndPort host = InetAddressAndPort.getByName("127.0.0.9"); - final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "r1"); // default - - try - { - setNodeLive(host); - - Files.copy(effectiveFile, backupFile); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC2:RAC2")); // add this line if not yet there - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "r1"); // unchanged - - setNodeShutdown(host); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "DC2:RAC2")); // add this line if not yet there - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC2", "RAC2"); // changed - } - finally - { - restoreOrigConfigFile(); - setNodeShutdown(host); - } - } - - /** - * Test that removing a host from the configuration file changes the host rack only if the host - * is not live. The original configuration file contains 127.0.0.2=DC1:RAC2 and default=DC1:r1 so removing - * this host should result in a different rack if the host is not live. - */ - @Test - public void testRemoveHost() throws Exception - { - final InetAddressAndPort host = InetAddressAndPort.getByName("127.0.0.2"); - final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC2"); - - try - { - setNodeLive(host); - - Files.copy(effectiveFile, backupFile); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "")); // removes line if found - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "RAC2"); // unchanged - - setNodeShutdown(host); - replaceConfigFile(Collections.singletonMap(host.getHostAddressAndPort(), "")); // removes line if found - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "r1"); // default - } - finally - { - restoreOrigConfigFile(); - setNodeShutdown(host); - } - } - - /** - * Test that we can change the default only if this does not result in any live node changing dc or rack. - * The configuration file contains default=DC1:r1 and we change it to default=DC2:r2. Let's use host 127.0.0.9 - * since it is not in the configuration file. - */ - @Test - public void testChangeDefault() throws Exception - { - final InetAddressAndPort host = InetAddressAndPort.getByName("127.0.0.9"); - final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "r1"); // default - - try - { - setNodeLive(host); - - Files.copy(effectiveFile, backupFile); - replaceConfigFile(Collections.singletonMap("default", "DC2:r2")); // change default - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC1", "r1"); // unchanged - - setNodeShutdown(host); - replaceConfigFile(Collections.singletonMap("default", "DC2:r2")); // change default again (refresh file update) - - Thread.sleep(1500); - checkEndpoint(snitch, host.getHostAddressAndPort(), "DC2", "r2"); // default updated - } - finally - { - restoreOrigConfigFile(); - setNodeShutdown(host); - } + // make a suitably escaped key=value string to write to a properties file + return key.replaceAll(Matcher.quoteReplacement(":"), "\\\\:") + '=' + value; } } diff --git a/test/unit/org/apache/cassandra/locator/ReplicaPlansTest.java b/test/unit/org/apache/cassandra/locator/ReplicaPlansTest.java index 8231b03a48..7f946abbae 100644 --- a/test/unit/org/apache/cassandra/locator/ReplicaPlansTest.java +++ b/test/unit/org/apache/cassandra/locator/ReplicaPlansTest.java @@ -27,6 +27,11 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Token; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.StubClusterMetadataService; + +import org.junit.Before; import org.junit.Test; import java.util.Map; @@ -43,6 +48,7 @@ public class ReplicaPlansTest DatabaseDescriptor.daemonInitialization(); } + // TODO replace use of snitch in determining counts per DC with directory lookup static class Snitch extends AbstractNetworkTopologySnitch { final Set dc1; @@ -63,13 +69,19 @@ public class ReplicaPlansTest } } + @Before + public void setup() + { + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); + } + private static Keyspace ks(Set dc1, Map replication) { replication = ImmutableMap.builder().putAll(replication).put("class", "NetworkTopologyStrategy").build(); Keyspace keyspace = Keyspace.mockKS(KeyspaceMetadata.create("blah", KeyspaceParams.create(false, replication))); Snitch snitch = new Snitch(dc1); DatabaseDescriptor.setEndpointSnitch(snitch); - keyspace.getReplicationStrategy().snitch = snitch; return keyspace; } @@ -89,7 +101,7 @@ public class ReplicaPlansTest Keyspace ks = ks(ImmutableSet.of(EP1, EP2, EP3), ImmutableMap.of("DC1", "3", "DC2", "3")); EndpointsForToken natural = EndpointsForToken.of(token, full(EP1), full(EP2), full(EP3), full(EP4), full(EP5), full(EP6)); EndpointsForToken pending = EndpointsForToken.empty(token); - ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(ks, ConsistencyLevel.EACH_QUORUM, natural, pending, Predicates.alwaysTrue(), ReplicaPlans.writeNormal); + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(ks, ConsistencyLevel.EACH_QUORUM, (cm) -> natural, (cm) -> pending, null, Predicates.alwaysTrue(), ReplicaPlans.writeNormal); assertEquals(natural, plan.liveAndDown); assertEquals(natural, plan.live); assertEquals(natural, plan.contacts()); @@ -99,7 +111,7 @@ public class ReplicaPlansTest Keyspace ks = ks(ImmutableSet.of(EP1, EP2, EP3), ImmutableMap.of("DC1", "3", "DC2", "3")); EndpointsForToken natural = EndpointsForToken.of(token, full(EP1), full(EP2), trans(EP3), full(EP4), full(EP5), trans(EP6)); EndpointsForToken pending = EndpointsForToken.empty(token); - ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(ks, ConsistencyLevel.EACH_QUORUM, natural, pending, Predicates.alwaysTrue(), ReplicaPlans.writeNormal); + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(ks, ConsistencyLevel.EACH_QUORUM, (cm) -> natural, (cm) -> pending, Epoch.FIRST, Predicates.alwaysTrue(), ReplicaPlans.writeNormal); assertEquals(natural, plan.liveAndDown); assertEquals(natural, plan.live); EndpointsForToken expectContacts = EndpointsForToken.of(token, full(EP1), full(EP2), full(EP4), full(EP5)); diff --git a/test/unit/org/apache/cassandra/locator/ReplicationFactorTest.java b/test/unit/org/apache/cassandra/locator/ReplicationFactorTest.java index 55f54ad3ca..f0e5e071d2 100644 --- a/test/unit/org/apache/cassandra/locator/ReplicationFactorTest.java +++ b/test/unit/org/apache/cassandra/locator/ReplicationFactorTest.java @@ -23,7 +23,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.assertj.core.api.Assertions; import static org.junit.Assert.assertEquals; @@ -35,7 +35,7 @@ public class ReplicationFactorTest { DatabaseDescriptor.daemonInitialization(); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - Gossiper.instance.start(1); + ClusterMetadataTestHelper.setInstanceForTest(); } @Test diff --git a/test/unit/org/apache/cassandra/locator/ReplicationStrategyEndpointCacheTest.java b/test/unit/org/apache/cassandra/locator/ReplicationStrategyEndpointCacheTest.java deleted file mode 100644 index 6ccfcde23e..0000000000 --- a/test/unit/org/apache/cassandra/locator/ReplicationStrategyEndpointCacheTest.java +++ /dev/null @@ -1,107 +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.locator; - -import org.apache.commons.lang3.StringUtils; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.Util; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.schema.KeyspaceParams; - -public class ReplicationStrategyEndpointCacheTest -{ - private TokenMetadata tmd; - private Token searchToken; - private AbstractReplicationStrategy strategy; - public static final String KEYSPACE = "ReplicationStrategyEndpointCacheTest"; - - @BeforeClass - public static void defineSchema() - { - SchemaLoader.prepareServer(); - SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(5)); - } - - public void setup() throws Exception - { - tmd = new TokenMetadata(); - searchToken = new BigIntegerToken(String.valueOf(15)); - strategy = getStrategyWithNewTokenMetadata(Keyspace.open(KEYSPACE).getReplicationStrategy(), tmd); - - for (int i = 1; i <= 8; i++) - tmd.updateNormalToken(new BigIntegerToken(String.valueOf(i * 10)), InetAddressAndPort.getByName("127.0.0." + i)); - } - - @Test - public void testEndpointsWereCached() throws Exception - { - setup(); - Util.assertRCEquals(strategy.getNaturalReplicasForToken(searchToken), strategy.getNaturalReplicasForToken(searchToken)); - } - - @Test - public void testCacheRespectsTokenChanges() throws Exception - { - setup(); - EndpointsForToken initial; - EndpointsForToken replicas; - - replicas = strategy.getNaturalReplicasForToken(searchToken); - assert replicas.size() == 5 : StringUtils.join(replicas, ","); - - // test token addition, in DC2 before existing token - initial = strategy.getNaturalReplicasForToken(searchToken); - tmd.updateNormalToken(new BigIntegerToken(String.valueOf(35)), InetAddressAndPort.getByName("127.0.0.5")); - replicas = strategy.getNaturalReplicasForToken(searchToken); - assert replicas.size() == 5 : StringUtils.join(replicas, ","); - Util.assertNotRCEquals(replicas, initial); - - // test token removal, newly created token - initial = strategy.getNaturalReplicasForToken(searchToken); - tmd.removeEndpoint(InetAddressAndPort.getByName("127.0.0.5")); - replicas = strategy.getNaturalReplicasForToken(searchToken); - assert replicas.size() == 5 : StringUtils.join(replicas, ","); - assert !replicas.endpoints().contains(InetAddressAndPort.getByName("127.0.0.5")); - Util.assertNotRCEquals(replicas, initial); - - // test token change - initial = strategy.getNaturalReplicasForToken(searchToken); - //move .8 after search token but before other DC3 - tmd.updateNormalToken(new BigIntegerToken(String.valueOf(25)), InetAddressAndPort.getByName("127.0.0.8")); - replicas = strategy.getNaturalReplicasForToken(searchToken); - assert replicas.size() == 5 : StringUtils.join(replicas, ","); - Util.assertNotRCEquals(replicas, initial); - } - - private AbstractReplicationStrategy getStrategyWithNewTokenMetadata(AbstractReplicationStrategy strategy, TokenMetadata newTmd) throws ConfigurationException - { - return AbstractReplicationStrategy.createReplicationStrategy( - strategy.keyspaceName, - AbstractReplicationStrategy.getClass(strategy.getClass().getName()), - newTmd, - strategy.snitch, - strategy.configOptions); - } - -} diff --git a/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java b/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java index 0204b22d70..8357035000 100644 --- a/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java +++ b/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java @@ -19,44 +19,42 @@ package org.apache.cassandra.locator; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.UUID; +import java.util.concurrent.ExecutionException; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.ExpectedException; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.OrderPreservingPartitioner; +import org.apache.cassandra.dht.*; import org.apache.cassandra.dht.OrderPreservingPartitioner.StringToken; import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; -import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.tcm.transformations.Register; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.ClientWarn; -import org.apache.cassandra.service.PendingRangeCalculatorService; -import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.service.StorageServiceAccessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeVersion; import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.FBUtilities; +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; +import static org.apache.cassandra.ServerTestUtils.recreateCMS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -66,10 +64,22 @@ public class SimpleStrategyTest public static final String KEYSPACE1 = "SimpleStrategyTest"; public static final String MULTIDC = "MultiDCSimpleStrategyTest"; + static + { + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + } + @BeforeClass public static void defineSchema() { - SchemaLoader.prepareServer(); + DatabaseDescriptor.daemonInitialization(); + } + + public static void withPartitioner(IPartitioner partitioner) + { + DatabaseDescriptor.setPartitionerUnsafe(partitioner); + ServerTestUtils.prepareServerNoRegister(); + recreateCMS(); SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple(1)); SchemaLoader.createKeyspace(MULTIDC, KeyspaceParams.simple(3)); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); @@ -90,8 +100,9 @@ public class SimpleStrategyTest @Test public void testBigIntegerEndpoints() throws UnknownHostException { - List endpointTokens = new ArrayList(); - List keyTokens = new ArrayList(); + withPartitioner(RandomPartitioner.instance); + List endpointTokens = new ArrayList<>(); + List keyTokens = new ArrayList<>(); for (int i = 0; i < 5; i++) { endpointTokens.add(new BigIntegerToken(String.valueOf(10 * i))); keyTokens.add(new BigIntegerToken(String.valueOf(10 * i + 5))); @@ -103,7 +114,7 @@ public class SimpleStrategyTest public void testStringEndpoints() throws UnknownHostException { IPartitioner partitioner = OrderPreservingPartitioner.instance; - + withPartitioner(partitioner); List endpointTokens = new ArrayList(); List keyTokens = new ArrayList(); for (int i = 0; i < 5; i++) { @@ -116,31 +127,29 @@ public class SimpleStrategyTest @Test public void testMultiDCSimpleStrategyEndpoints() throws UnknownHostException { + withPartitioner(Murmur3Partitioner.instance); IEndpointSnitch snitch = new PropertyFileSnitch(); DatabaseDescriptor.setEndpointSnitch(snitch); - TokenMetadata metadata = new TokenMetadata(); - - AbstractReplicationStrategy strategy = getStrategy(MULTIDC, metadata, snitch); - // Topology taken directly from the topology_test.test_size_estimates_multidc dtest that regressed Multimap dc1 = HashMultimap.create(); dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new Murmur3Partitioner.LongToken(-6639341390736545756L)); dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new Murmur3Partitioner.LongToken(-2688160409776496397L)); dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new Murmur3Partitioner.LongToken(-2506475074448728501L)); dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new Murmur3Partitioner.LongToken(8473270337963525440L)); - metadata.updateNormalTokens(dc1); + dc1.asMap().forEach((e, t) -> ClusterMetadataTestHelper.addEndpoint(e, t, "dc1", "rack1")); Multimap dc2 = HashMultimap.create(); dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new Murmur3Partitioner.LongToken(-3736333188524231709L)); dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new Murmur3Partitioner.LongToken(8673615181726552074L)); - metadata.updateNormalTokens(dc2); + dc2.asMap().forEach((e, t) -> ClusterMetadataTestHelper.addEndpoint(e, t, "dc2", "rack1")); Map primaryCount = new HashMap<>(); Map replicaCount = new HashMap<>(); - for (Token t : metadata.sortedTokens()) + ClusterMetadata metadata = ClusterMetadata.current(); + for (Token t : metadata.tokenMap.tokens()) { - EndpointsForToken replicas = strategy.getNaturalReplicasForToken(t); + EndpointsForToken replicas = ClusterMetadataTestHelper.getNaturalReplicasForToken(MULTIDC, t).get(); primaryCount.compute(replicas.get(0).endpoint(), (k, v) -> (v == null) ? 1 : v + 1); for (Replica replica : replicas) replicaCount.compute(replica.endpoint(), (k, v) -> (v == null) ? 1 : v + 1); @@ -158,23 +167,23 @@ public class SimpleStrategyTest // make sure that the Strategy picks the right endpoints for the keys. private void verifyGetNaturalEndpoints(Token[] endpointTokens, Token[] keyTokens) throws UnknownHostException { - TokenMetadata tmd; - AbstractReplicationStrategy strategy; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) + List hosts = new ArrayList<>(); + for (int i = 0; i < endpointTokens.length; i++) { - tmd = new TokenMetadata(); - strategy = getStrategy(keyspaceName, tmd, new SimpleSnitch()); - List hosts = new ArrayList<>(); - for (int i = 0; i < endpointTokens.length; i++) - { - InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0." + String.valueOf(i + 1)); - tmd.updateNormalToken(endpointTokens[i], ep); - hosts.add(ep); - } + InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0." + (i + 1)); + ClusterMetadataTestHelper.addEndpoint(ep, endpointTokens[i]); + hosts.add(ep); + } + AbstractReplicationStrategy strategy; + for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces() + .without(SchemaConstants.METADATA_KEYSPACE_NAME) + .names()) + { + strategy = getStrategy(keyspaceName); for (int i = 0; i < keyTokens.length; i++) { - EndpointsForToken replicas = strategy.getNaturalReplicasForToken(keyTokens[i]); + EndpointsForToken replicas = ClusterMetadataTestHelper.getNaturalReplicasForToken(keyspaceName, keyTokens[i]).get(); assertEquals(strategy.getReplicationFactor().allReplicas, replicas.size()); List correctEndpoints = new ArrayList<>(); for (int j = 0; j < replicas.size(); j++) @@ -185,12 +194,11 @@ public class SimpleStrategyTest } @Test - public void testGetEndpointsDuringBootstrap() throws UnknownHostException + public void testGetEndpointsDuringBootstrap() throws UnknownHostException, ExecutionException, InterruptedException { + withPartitioner(RandomPartitioner.instance); // the token difference will be RING_SIZE * 2. final int RING_SIZE = 10; - TokenMetadata tmd = new TokenMetadata(); - TokenMetadata oldTmd = StorageServiceAccessor.setTokenMetadata(tmd); Token[] endpointTokens = new Token[RING_SIZE]; Token[] keyTokens = new Token[RING_SIZE]; @@ -205,33 +213,43 @@ public class SimpleStrategyTest for (int i = 0; i < endpointTokens.length; i++) { InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0." + String.valueOf(i + 1)); - tmd.updateNormalToken(endpointTokens[i], ep); + ClusterMetadataTestHelper.addEndpoint(ep, endpointTokens[i]); hosts.add(ep); } // bootstrap at the end of the ring Token bsToken = new BigIntegerToken(String.valueOf(210)); InetAddressAndPort bootstrapEndpoint = InetAddressAndPort.getByName("127.0.0.11"); - tmd.addBootstrapTokens(Collections.singleton(bsToken), bootstrapEndpoint); + Location l = new Location("dc1", "rack1"); + ClusterMetadataTestHelper.commit(new Register(ClusterMetadataTestHelper.addr(bootstrapEndpoint), l, NodeVersion.CURRENT)); + ClusterMetadataTestHelper.lazyJoin(bootstrapEndpoint, bsToken) + .prepareJoin() + .startJoin(); AbstractReplicationStrategy strategy = null; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) + ClusterMetadata metadata = ClusterMetadata.current(); + for (String keyspaceName : Schema.instance.getNonLocalStrategyKeyspaces().names()) { - strategy = getStrategy(keyspaceName, tmd, new SimpleSnitch()); + ReplicationParams replication = Schema.instance.getKeyspaceMetadata(keyspaceName).params.replication; + if (replication.isMeta()) + continue; - PendingRangeCalculatorService.calculatePendingRanges(strategy, keyspaceName); + strategy = getStrategy(keyspaceName); int replicationFactor = strategy.getReplicationFactor().allReplicas; for (int i = 0; i < keyTokens.length; i++) { - EndpointsForToken replicas = tmd.getWriteEndpoints(keyTokens[i], keyspaceName, strategy.getNaturalReplicasForToken(keyTokens[i])); + EndpointsForToken replicas = getWriteEndpoints(metadata, replication, keyTokens[i]); assertTrue(replicas.size() >= replicationFactor); for (int j = 0; j < replicationFactor; j++) { + InetAddressAndPort host = hosts.get((i + j + 1) % hosts.size()); //Check that the old nodes are definitely included - assertTrue(replicas.endpoints().contains(hosts.get((i + j + 1) % hosts.size()))); + assertTrue(String.format("%s should contain %s but it did not. RF=%d \n%s", + replicas, host, replicationFactor, metadata), + replicas.endpoints().contains(host)); } // bootstrapEndpoint should be in the endpoints for i in MAX-RF to MAX, but not in any earlier ep. @@ -241,8 +259,6 @@ public class SimpleStrategyTest assertTrue(replicas.endpoints().contains(bootstrapEndpoint)); } } - - StorageServiceAccessor.setTokenMetadata(oldTmd); } private static Token tk(long t) @@ -258,6 +274,7 @@ public class SimpleStrategyTest @Test public void transientReplica() throws Exception { + withPartitioner(Murmur3Partitioner.instance); IEndpointSnitch snitch = new SimpleSnitch(); DatabaseDescriptor.setEndpointSnitch(snitch); @@ -271,20 +288,19 @@ public class SimpleStrategyTest tokens.put(endpoints.get(1), tk(200)); tokens.put(endpoints.get(2), tk(300)); tokens.put(endpoints.get(3), tk(400)); - TokenMetadata metadata = new TokenMetadata(); - metadata.updateNormalTokens(tokens); + tokens.forEach(ClusterMetadataTestHelper::addEndpoint); Map configOptions = new HashMap(); + configOptions.put(ReplicationParams.CLASS, SimpleStrategy.class.getName()); configOptions.put("replication_factor", "3/1"); + SchemaLoader.createKeyspace("ks", KeyspaceParams.create(false, configOptions)); + Range range1 = range(Murmur3Partitioner.MINIMUM.token, 100); - SimpleStrategy strategy = new SimpleStrategy("ks", metadata, snitch, configOptions); - - Range range1 = range(400, 100); Util.assertRCEquals(EndpointsForToken.of(range1.right, Replica.fullReplica(endpoints.get(0), range1), Replica.fullReplica(endpoints.get(1), range1), Replica.transientReplica(endpoints.get(2), range1)), - strategy.getNaturalReplicasForToken(tk(99))); + ClusterMetadataTestHelper.getNaturalReplicasForToken("ks", tk(99)).get()); Range range2 = range(100, 200); @@ -292,7 +308,7 @@ public class SimpleStrategyTest Replica.fullReplica(endpoints.get(1), range2), Replica.fullReplica(endpoints.get(2), range2), Replica.transientReplica(endpoints.get(3), range2)), - strategy.getNaturalReplicasForToken(tk(101))); + ClusterMetadataTestHelper.getNaturalReplicasForToken("ks", tk(101)).get()); } @Rule @@ -301,6 +317,7 @@ public class SimpleStrategyTest @Test public void testSimpleStrategyThrowsConfigurationException() throws ConfigurationException, UnknownHostException { + withPartitioner(Murmur3Partitioner.instance); expectedEx.expect(ConfigurationException.class); expectedEx.expectMessage("SimpleStrategy requires a replication_factor strategy option."); @@ -316,51 +333,52 @@ public class SimpleStrategyTest tokens.put(endpoints.get(1), tk(200)); tokens.put(endpoints.get(2), tk(300)); - TokenMetadata metadata = new TokenMetadata(); - metadata.updateNormalTokens(tokens); - Map configOptions = new HashMap<>(); @SuppressWarnings("unused") - SimpleStrategy strategy = new SimpleStrategy("ks", metadata, snitch, configOptions); + SimpleStrategy strategy = new SimpleStrategy("ks", configOptions); } @Test public void shouldReturnNoEndpointsForEmptyRing() { - TokenMetadata metadata = new TokenMetadata(); - + withPartitioner(Murmur3Partitioner.instance); + HashMap configOptions = new HashMap<>(); configOptions.put("replication_factor", "1"); - SimpleStrategy strategy = new SimpleStrategy("ks", metadata, new SimpleSnitch(), configOptions); + SimpleStrategy strategy = new SimpleStrategy("ks", configOptions); - EndpointsForRange replicas = strategy.calculateNaturalReplicas(null, metadata); + EndpointsForRange replicas = strategy.calculateNaturalReplicas(null, new ClusterMetadata(Murmur3Partitioner.instance)); assertTrue(replicas.endpoints().isEmpty()); } @Test public void shouldWarnOnHigherReplicationFactorThanNodes() { + withPartitioner(Murmur3Partitioner.instance); HashMap configOptions = new HashMap<>(); configOptions.put("replication_factor", "2"); - SimpleStrategy strategy = new SimpleStrategy("ks", new TokenMetadata(), new SimpleSnitch(), configOptions); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), FBUtilities.getBroadcastAddressAndPort()); - + SimpleStrategy strategy = new SimpleStrategy("ks", configOptions); + ClusterMetadataTestHelper.addEndpoint(1); ClientWarn.instance.captureWarnings(); strategy.maybeWarnOnOptions(null); assertTrue(ClientWarn.instance.getWarnings().stream().anyMatch(s -> s.contains("Your replication factor"))); } - private AbstractReplicationStrategy getStrategy(String keyspaceName, TokenMetadata tmd, IEndpointSnitch snitch) + private AbstractReplicationStrategy getStrategy(String keyspaceName) { KeyspaceMetadata ksmd = Schema.instance.getKeyspaceMetadata(keyspaceName); - return AbstractReplicationStrategy.createReplicationStrategy( - keyspaceName, - ksmd.params.replication.klass, - tmd, - snitch, - ksmd.params.replication.options); + return AbstractReplicationStrategy.createReplicationStrategy(keyspaceName, + ksmd.params.replication.klass, + ksmd.params.replication.options); + } + + public static EndpointsForToken getWriteEndpoints(ClusterMetadata metadata, + ReplicationParams replicationParams, + Token token) + { + return metadata.placements.get(replicationParams).writes.forToken(token).get(); } } diff --git a/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java b/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java deleted file mode 100644 index c1b76b3bfc..0000000000 --- a/test/unit/org/apache/cassandra/locator/TokenMetadataTest.java +++ /dev/null @@ -1,399 +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.locator; - -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Iterators; -import com.google.common.collect.Multimap; - -import org.junit.BeforeClass; -import org.junit.Before; -import org.junit.Test; - -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.service.StorageService; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import static org.apache.cassandra.Util.token; - - -public class TokenMetadataTest -{ - public final static String ONE = "1"; - public final static String SIX = "6"; - - static TokenMetadata tmd; - - @BeforeClass - public static void beforeClass() throws Throwable - { - DatabaseDescriptor.daemonInitialization(); - } - - @Before - public void before() throws Throwable - { - tmd = new TokenMetadata(); - tmd.updateNormalToken(token(ONE), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateNormalToken(token(SIX), InetAddressAndPort.getByName("127.0.0.6")); - } - - private static void testRingIterator(ArrayList ring, String start, boolean includeMin, String... expected) - { - ArrayList actual = new ArrayList<>(); - Iterators.addAll(actual, TokenMetadata.ringIterator(ring, token(start), includeMin)); - assertEquals(actual.toString(), expected.length, actual.size()); - for (int i = 0; i < expected.length; i++) - assertEquals("Mismatch at index " + i + ": " + actual, token(expected[i]), actual.get(i)); - } - - /** - * This test is very likely (but not guaranteed) to fail if ring invalidations are ever allowed to interleave. - */ - @Test - public void testConcurrentInvalidation() throws InterruptedException - { - long startVersion = tmd.getRingVersion(); - - ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1); - - int invalidations = 1024; - - for (int i = 0; i < invalidations; i++) - pool.execute(() -> tmd.invalidateCachedRings()); - - pool.shutdown(); - - assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS)); - assertEquals(invalidations + startVersion, tmd.getRingVersion()); - } - - @Test - public void testRingIterator() - { - ArrayList ring = tmd.sortedTokens(); - testRingIterator(ring, "2", false, "6", "1"); - testRingIterator(ring, "7", false, "1", "6"); - testRingIterator(ring, "0", false, "1", "6"); - testRingIterator(ring, "", false, "1", "6"); - } - - @Test - public void testRingIteratorIncludeMin() - { - ArrayList ring = tmd.sortedTokens(); - testRingIterator(ring, "2", true, "6", "", "1"); - testRingIterator(ring, "7", true, "", "1", "6"); - testRingIterator(ring, "0", true, "1", "6", ""); - testRingIterator(ring, "", true, "1", "6", ""); - } - - @Test - public void testRingIteratorEmptyRing() - { - testRingIterator(new ArrayList(), "2", false); - } - - @Test - public void testTopologyUpdate_RackConsolidation() throws UnknownHostException - { - final InetAddressAndPort first = InetAddressAndPort.getByName("127.0.0.1"); - final InetAddressAndPort second = InetAddressAndPort.getByName("127.0.0.6"); - final String DATA_CENTER = "datacenter1"; - final String RACK1 = "rack1"; - final String RACK2 = "rack2"; - - DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) - { - return endpoint.equals(first) ? RACK1 : RACK2; - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - return DATA_CENTER; - } - - @Override - public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) - { - return 0; - } - }); - - tmd.updateNormalToken(token(ONE), first); - tmd.updateNormalToken(token(SIX), second); - - TokenMetadata tokenMetadata = tmd.cloneOnlyTokenMap(); - assertNotNull(tokenMetadata); - - TokenMetadata.Topology topology = tokenMetadata.getTopology(); - assertNotNull(topology); - - Multimap allEndpoints = topology.getDatacenterEndpoints(); - assertNotNull(allEndpoints); - assertTrue(allEndpoints.size() == 2); - assertTrue(allEndpoints.containsKey(DATA_CENTER)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(first)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(second)); - - Map> racks = topology.getDatacenterRacks(); - assertNotNull(racks); - assertTrue(racks.size() == 1); - assertTrue(racks.containsKey(DATA_CENTER)); - assertTrue(racks.get(DATA_CENTER).size() == 2); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK1)); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK2)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(first)); - assertTrue(racks.get(DATA_CENTER).get(RACK2).contains(second)); - - DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) - { - return RACK1; - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - return DATA_CENTER; - } - - @Override - public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) - { - return 0; - } - }); - - tokenMetadata.updateTopology(first); - topology = tokenMetadata.updateTopology(second); - - allEndpoints = topology.getDatacenterEndpoints(); - assertNotNull(allEndpoints); - assertTrue(allEndpoints.size() == 2); - assertTrue(allEndpoints.containsKey(DATA_CENTER)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(first)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(second)); - - racks = topology.getDatacenterRacks(); - assertNotNull(racks); - assertTrue(racks.size() == 1); - assertTrue(racks.containsKey(DATA_CENTER)); - assertTrue(racks.get(DATA_CENTER).size() == 2); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK1)); - assertFalse(racks.get(DATA_CENTER).containsKey(RACK2)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(first)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(second)); - } - - @Test - public void testTopologyUpdate_RackExpansion() throws UnknownHostException - { - final InetAddressAndPort first = InetAddressAndPort.getByName("127.0.0.1"); - final InetAddressAndPort second = InetAddressAndPort.getByName("127.0.0.6"); - final String DATA_CENTER = "datacenter1"; - final String RACK1 = "rack1"; - final String RACK2 = "rack2"; - - DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) - { - return RACK1; - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - return DATA_CENTER; - } - - @Override - public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) - { - return 0; - } - }); - - tmd.updateNormalToken(token(ONE), first); - tmd.updateNormalToken(token(SIX), second); - - TokenMetadata tokenMetadata = tmd.cloneOnlyTokenMap(); - assertNotNull(tokenMetadata); - - TokenMetadata.Topology topology = tokenMetadata.getTopology(); - assertNotNull(topology); - - Multimap allEndpoints = topology.getDatacenterEndpoints(); - assertNotNull(allEndpoints); - assertTrue(allEndpoints.size() == 2); - assertTrue(allEndpoints.containsKey(DATA_CENTER)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(first)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(second)); - - Map> racks = topology.getDatacenterRacks(); - assertNotNull(racks); - assertTrue(racks.size() == 1); - assertTrue(racks.containsKey(DATA_CENTER)); - assertTrue(racks.get(DATA_CENTER).size() == 2); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK1)); - assertFalse(racks.get(DATA_CENTER).containsKey(RACK2)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(first)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(second)); - - DatabaseDescriptor.setEndpointSnitch(new AbstractEndpointSnitch() - { - @Override - public String getRack(InetAddressAndPort endpoint) - { - return endpoint.equals(first) ? RACK1 : RACK2; - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - return DATA_CENTER; - } - - @Override - public int compareEndpoints(InetAddressAndPort target, Replica a1, Replica a2) - { - return 0; - } - }); - - topology = tokenMetadata.updateTopology(); - - allEndpoints = topology.getDatacenterEndpoints(); - assertNotNull(allEndpoints); - assertTrue(allEndpoints.size() == 2); - assertTrue(allEndpoints.containsKey(DATA_CENTER)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(first)); - assertTrue(allEndpoints.get(DATA_CENTER).contains(second)); - - racks = topology.getDatacenterRacks(); - assertNotNull(racks); - assertTrue(racks.size() == 1); - assertTrue(racks.containsKey(DATA_CENTER)); - assertTrue(racks.get(DATA_CENTER).size() == 2); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK1)); - assertTrue(racks.get(DATA_CENTER).containsKey(RACK2)); - assertTrue(racks.get(DATA_CENTER).get(RACK1).contains(first)); - assertTrue(racks.get(DATA_CENTER).get(RACK2).contains(second)); - } - - @Test - public void testEndpointSizes() throws UnknownHostException - { - final InetAddressAndPort first = InetAddressAndPort.getByName("127.0.0.1"); - final InetAddressAndPort second = InetAddressAndPort.getByName("127.0.0.6"); - - tmd.updateNormalToken(token(ONE), first); - tmd.updateNormalToken(token(SIX), second); - - TokenMetadata tokenMetadata = tmd.cloneOnlyTokenMap(); - assertNotNull(tokenMetadata); - - tokenMetadata.updateHostId(UUID.randomUUID(), first); - tokenMetadata.updateHostId(UUID.randomUUID(), second); - - assertEquals(2, tokenMetadata.getSizeOfAllEndpoints()); - assertEquals(0, tokenMetadata.getSizeOfLeavingEndpoints()); - assertEquals(0, tokenMetadata.getSizeOfMovingEndpoints()); - - tokenMetadata.addLeavingEndpoint(first); - assertEquals(1, tokenMetadata.getSizeOfLeavingEndpoints()); - - tokenMetadata.removeEndpoint(first); - assertEquals(0, tokenMetadata.getSizeOfLeavingEndpoints()); - assertEquals(1, tokenMetadata.getSizeOfAllEndpoints()); - - tokenMetadata.addMovingEndpoint(token(SIX), second); - assertEquals(1, tokenMetadata.getSizeOfMovingEndpoints()); - - tokenMetadata.removeFromMoving(second); - assertEquals(0, tokenMetadata.getSizeOfMovingEndpoints()); - - tokenMetadata.removeEndpoint(second); - assertEquals(0, tokenMetadata.getSizeOfAllEndpoints()); - assertEquals(0, tokenMetadata.getSizeOfLeavingEndpoints()); - assertEquals(0, tokenMetadata.getSizeOfMovingEndpoints()); - } - - - @Test - public void testRemoveEndpointTokenChange() throws Exception - { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - - Collection tokens = new HashSet<>(); - tokens.add(DatabaseDescriptor.getPartitioner().getRandomToken()); - tokens.add(DatabaseDescriptor.getPartitioner().getRandomToken()); - - InetAddressAndPort ep1 = InetAddressAndPort.getByName("127.0.0.1"); - InetAddressAndPort ep2 = InetAddressAndPort.getByName("127.0.0.2"); - - Multimap endpointTokens = HashMultimap.create(); - for (Token token : tokens) - endpointTokens.put(ep1, token); - - endpointTokens.put(ep2, DatabaseDescriptor.getPartitioner().getRandomToken()); - - long ver = metadata.getRingVersion(); - metadata.updateNormalTokens(endpointTokens); - assertTrue(metadata.getRingVersion() > ver); - - // Remove a normal endpoint - assertTrue(metadata.isMember(ep2)); - ver = metadata.getRingVersion(); - metadata.removeEndpoint(ep2); - assertFalse(metadata.isMember(ep2)); - assertTrue(metadata.getRingVersion() > ver); - - // Remove a non-exist endpoint (e.g. proxy node is not part of token metadata) - InetAddressAndPort ep3 = InetAddressAndPort.getByName("127.0.0.3"); - assertFalse(metadata.isMember(ep3)); - ver = metadata.getRingVersion(); - metadata.removeEndpoint(ep3); - assertEquals(ver, metadata.getRingVersion()); - } -} diff --git a/test/unit/org/apache/cassandra/locator/AbstractReplicationStrategyTest.java b/test/unit/org/apache/cassandra/locator/WithPartitioner.java similarity index 50% rename from test/unit/org/apache/cassandra/locator/AbstractReplicationStrategyTest.java rename to test/unit/org/apache/cassandra/locator/WithPartitioner.java index 62206ce8bb..94a07a6e5f 100644 --- a/test/unit/org/apache/cassandra/locator/AbstractReplicationStrategyTest.java +++ b/test/unit/org/apache/cassandra/locator/WithPartitioner.java @@ -18,28 +18,29 @@ package org.apache.cassandra.locator; -import org.junit.Test; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.tcm.ClusterMetadataService; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class AbstractReplicationStrategyTest +public class WithPartitioner implements AutoCloseable { - @Test - public void testReplicaCache() + private ClusterMetadataService toRestore; + + public WithPartitioner(IPartitioner partitioner) { - AbstractReplicationStrategy.ReplicaCache cache = new AbstractReplicationStrategy.ReplicaCache<>(); + toRestore = ClusterMetadataService.instance(); + DatabaseDescriptor.setPartitionerUnsafe(partitioner); + ClusterMetadataService withNewPartitioner = ClusterMetadataTestHelper.instanceForTest(); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(withNewPartitioner); + } - cache.put(10, 1, 1); - assertEquals(1, (int)cache.get(10, 1)); - assertNull(cache.get(9,1)); // get with old ringversion, return null to force a recalculation - assertNull(cache.get(11,1)); // newer ringVersion - cache gets cleared - assertNull(cache.get(10,1)); // and make sure the map got cleared - - cache.put(11, 1, 100); - cache.put(10, 1, 99); - assertEquals(100, (int)cache.get(11, 1)); - assertNull(cache.get(12, 55)); - assertNull(cache.get(11, 1)); + @Override + public void close() + { + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(toRestore); } } + diff --git a/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java b/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java index 37cf8b1156..24fdc8b475 100644 --- a/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java +++ b/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java @@ -78,8 +78,6 @@ public class TrieMemtableMetricsTest extends SchemaLoader }); MEMTABLE_SHARD_COUNT.setInt(NUM_SHARDS); - SchemaLoader.loadSchema(); - EmbeddedCassandraService cassandra = new EmbeddedCassandraService(); cassandra.start(); diff --git a/test/unit/org/apache/cassandra/net/ConnectionTest.java b/test/unit/org/apache/cassandra/net/ConnectionTest.java index 778dc41fc8..ff11f9dbb0 100644 --- a/test/unit/org/apache/cassandra/net/ConnectionTest.java +++ b/test/unit/org/apache/cassandra/net/ConnectionTest.java @@ -58,6 +58,7 @@ import io.netty.channel.ChannelPromise; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.exceptions.UnknownColumnException; import org.apache.cassandra.io.IVersionedAsymmetricSerializer; @@ -118,6 +119,7 @@ public class ConnectionTest public static void startup() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); CommitLog.instance.start(); } @@ -359,6 +361,25 @@ public class ConnectionTest .withApplicationSendQueueCapacityInBytes(1 << 16)), (inbound, outbound, endpoint) -> { + unsafeSetSerializer(Verb._TEST_1, () -> new IVersionedSerializer() + { + public void serialize(Object o, DataOutputPlus out, int version) throws IOException + { + for (int i = 0; i <= 4 << 16; i += 8L) + out.writeLong(1L); + } + + public Object deserialize(DataInputPlus in, int version) throws IOException + { + in.skipBytesFully(4 << 16); + return null; + } + + public long serializedSize(Object o, int version) + { + return 4 << 16; + } + }); CountDownLatch done = new CountDownLatch(1); Message message = Message.out(Verb._TEST_1, new Object()); MessagingService.instance().callbacks.addWithExpiration(new RequestCallback() @@ -383,25 +404,7 @@ public class ConnectionTest }, message, endpoint); AtomicInteger delivered = new AtomicInteger(); - unsafeSetSerializer(Verb._TEST_1, () -> new IVersionedSerializer() - { - public void serialize(Object o, DataOutputPlus out, int version) throws IOException - { - for (int i = 0 ; i <= 4 << 16 ; i += 8L) - out.writeLong(1L); - } - public Object deserialize(DataInputPlus in, int version) throws IOException - { - in.skipBytesFully(4 << 16); - return null; - } - - public long serializedSize(Object o, int version) - { - return 4 << 16; - } - }); unsafeSetHandler(Verb._TEST_1, () -> msg -> delivered.incrementAndGet()); outbound.enqueue(message); Assert.assertTrue(done.await(10, SECONDS)); @@ -433,9 +436,6 @@ public class ConnectionTest CountDownLatch receiveDone = new CountDownLatch(90); AtomicInteger serialized = new AtomicInteger(); - Message message = Message.builder(Verb._TEST_1, new Object()) - .withExpiresAt(nanoTime() + SECONDS.toNanos(30L)) - .build(); unsafeSetSerializer(Verb._TEST_1, () -> new IVersionedSerializer() { public void serialize(Object o, DataOutputPlus out, int version) throws IOException @@ -463,6 +463,9 @@ public class ConnectionTest return 1; } }); + Message message = Message.builder(Verb._TEST_1, new Object()) + .withExpiresAt(nanoTime() + SECONDS.toNanos(30L)) + .build(); unsafeSetHandler(Verb._TEST_1, () -> msg -> receiveDone.countDown()); for (int i = 0 ; i < count ; ++i) diff --git a/test/unit/org/apache/cassandra/net/FramingTest.java b/test/unit/org/apache/cassandra/net/FramingTest.java index 6695012b8c..97c70cac8a 100644 --- a/test/unit/org/apache/cassandra/net/FramingTest.java +++ b/test/unit/org/apache/cassandra/net/FramingTest.java @@ -38,6 +38,7 @@ import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.util.DataInputPlus; @@ -61,6 +62,7 @@ public class FramingTest public static void begin() throws NoSuchFieldException, IllegalAccessException { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); Verb._TEST_1.unsafeSetSerializer(() -> new IVersionedSerializer() { diff --git a/test/unit/org/apache/cassandra/net/HandshakeTest.java b/test/unit/org/apache/cassandra/net/HandshakeTest.java index 46e399259d..f2fa960566 100644 --- a/test/unit/org/apache/cassandra/net/HandshakeTest.java +++ b/test/unit/org/apache/cassandra/net/HandshakeTest.java @@ -29,33 +29,34 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import com.google.common.net.InetAddresses; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.Future; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.gms.GossipDigestSyn; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess; import org.apache.cassandra.security.DefaultSslContextFactory; import org.apache.cassandra.utils.concurrent.AsyncPromise; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; - +import static org.apache.cassandra.net.ConnectionType.SMALL_MESSAGES; import static org.apache.cassandra.net.MessagingService.current_version; import static org.apache.cassandra.net.MessagingService.minimum_version; -import static org.apache.cassandra.net.ConnectionType.SMALL_MESSAGES; -import static org.apache.cassandra.net.OutboundConnectionInitiator.*; - +import static org.apache.cassandra.net.OutboundConnectionInitiator.Result; +import static org.apache.cassandra.net.OutboundConnectionInitiator.SslFallbackConnectionType; +import static org.apache.cassandra.net.OutboundConnectionInitiator.initiateMessaging; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; // TODO: test failure due to exception, timeout, etc public class HandshakeTest @@ -69,6 +70,7 @@ public class HandshakeTest public static void startup() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); CommitLog.instance.start(); } diff --git a/test/unit/org/apache/cassandra/net/MessageSerializationPropertyTest.java b/test/unit/org/apache/cassandra/net/MessageSerializationPropertyTest.java index 88a45af510..34387242f1 100644 --- a/test/unit/org/apache/cassandra/net/MessageSerializationPropertyTest.java +++ b/test/unit/org/apache/cassandra/net/MessageSerializationPropertyTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadQuery; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.IVersionedAsymmetricSerializer; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; @@ -59,6 +60,7 @@ public class MessageSerializationPropertyTest implements Serializable CLOCK_MONOTONIC_APPROX.setString(FixedMonotonicClock.class.getName()); DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); } /** diff --git a/test/unit/org/apache/cassandra/net/MessageTest.java b/test/unit/org/apache/cassandra/net/MessageTest.java index 0228608ea4..8e89973aa7 100644 --- a/test/unit/org/apache/cassandra/net/MessageTest.java +++ b/test/unit/org/apache/cassandra/net/MessageTest.java @@ -28,6 +28,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.io.IVersionedSerializer; @@ -36,6 +37,7 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.tracing.Tracing.TraceType; import org.apache.cassandra.utils.FBUtilities; @@ -58,6 +60,7 @@ public class MessageTest @BeforeClass public static void setUpClass() throws Exception { + ServerTestUtils.prepareServer(); DatabaseDescriptor.daemonInitialization(); DatabaseDescriptor.setCrossNodeTimeout(true); @@ -91,6 +94,7 @@ public class MessageTest { Message msg = Message.builder(Verb._TEST_2, 37) + .withEpoch(Epoch.EMPTY) .withId(1) .from(FBUtilities.getLocalAddressAndPort()) .withCreatedAt(approxTime.now()) @@ -118,11 +122,11 @@ public class MessageTest // should return -1 - fail to infer size - for all lengths of buffer until payload length can be read for (int limit = 0; limit < serializedSize - payloadSize; limit++) - assertEquals(-1, serializer.inferMessageSize(buffer, 0, limit)); + assertEquals(-1, serializer.inferMessageSize(buffer, 0, limit, version)); // once payload size can be read, should correctly infer message size for (int limit = serializedSize - payloadSize; limit < serializedSize; limit++) - assertEquals(serializedSize, serializer.inferMessageSize(buffer, 0, limit)); + assertEquals(serializedSize, serializer.inferMessageSize(buffer, 0, limit, version)); } } @@ -138,6 +142,7 @@ public class MessageTest Message msg = Message.builder(Verb._TEST_1, noPayload) + .withEpoch(Epoch.EMPTY) .withId(1) .from(from) .withCreatedAt(createAtNanos) @@ -164,6 +169,7 @@ public class MessageTest { Message msg = Message.builder(Verb._TEST_1, noPayload) + .withEpoch(Epoch.EMPTY) .withId(1) .from(FBUtilities.getLocalAddressAndPort()) .withCreatedAt(approxTime.now()) @@ -207,7 +213,7 @@ public class MessageTest @Test public void testBuilderNotAddTraceHeaderWithNoTraceSession() { - Message msg = Message.builder(Verb._TEST_1, noPayload).withTracingParams().build(); + Message msg = Message.builder(Verb._TEST_1, noPayload).withTracingParams().withEpoch(Epoch.EMPTY).build(); assertNull(msg.header.traceSession()); } @@ -219,6 +225,7 @@ public class MessageTest Message msg = Message.builder(Verb._TEST_1, noPayload) + .withEpoch(Epoch.EMPTY) .withId(1) .from(from) .withCustomParam("custom1", "custom1value".getBytes(StandardCharsets.UTF_8)) @@ -248,7 +255,7 @@ public class MessageTest try { TimeUUID sessionId = Tracing.instance.newSession(traceType); - Message msg = Message.builder(Verb._TEST_1, noPayload).withTracingParams().build(); + Message msg = Message.builder(Verb._TEST_1, noPayload).withEpoch(Epoch.FIRST).withTracingParams().build(); assertEquals(sessionId, msg.header.traceSession()); assertEquals(traceType, msg.header.traceType()); } diff --git a/test/unit/org/apache/cassandra/net/MessagingServiceTest.java b/test/unit/org/apache/cassandra/net/MessagingServiceTest.java index 32d5050388..f1087efb10 100644 --- a/test/unit/org/apache/cassandra/net/MessagingServiceTest.java +++ b/test/unit/org/apache/cassandra/net/MessagingServiceTest.java @@ -51,6 +51,7 @@ import org.apache.cassandra.auth.IInternodeAuthenticator; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.metrics.MessagingMetrics; @@ -113,6 +114,7 @@ public class MessagingServiceTest public static void beforeClass() throws UnknownHostException { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); CommitLog.instance.start(); DatabaseDescriptor.setBroadcastAddress(InetAddress.getByName("127.0.0.1")); originalAuthenticator = DatabaseDescriptor.getInternodeAuthenticator(); diff --git a/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java b/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java index 8023162cf8..ee2e0ad34c 100644 --- a/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java +++ b/test/unit/org/apache/cassandra/net/MockMessagingServiceTest.java @@ -23,7 +23,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.service.StorageService; @@ -41,7 +41,7 @@ public class MockMessagingServiceTest @BeforeClass public static void initCluster() throws ConfigurationException { - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServerNoRegister(); StorageService.instance.initServer(); } diff --git a/test/unit/org/apache/cassandra/net/OutboundConnectionsTest.java b/test/unit/org/apache/cassandra/net/OutboundConnectionsTest.java index 538636a4af..6acdf0ce21 100644 --- a/test/unit/org/apache/cassandra/net/OutboundConnectionsTest.java +++ b/test/unit/org/apache/cassandra/net/OutboundConnectionsTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.gms.GossipDigestSyn; import org.apache.cassandra.io.IVersionedAsymmetricSerializer; import org.apache.cassandra.io.IVersionedSerializer; @@ -75,6 +76,7 @@ public class OutboundConnectionsTest public static void before() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); CommitLog.instance.start(); } diff --git a/test/unit/org/apache/cassandra/net/OutboundMessageQueueTest.java b/test/unit/org/apache/cassandra/net/OutboundMessageQueueTest.java index 5cc9d1ab0b..68f65e386b 100644 --- a/test/unit/org/apache/cassandra/net/OutboundMessageQueueTest.java +++ b/test/unit/org/apache/cassandra/net/OutboundMessageQueueTest.java @@ -30,6 +30,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.utils.FreeRunningClock; import static org.apache.cassandra.net.NoPayload.noPayload; @@ -42,6 +43,7 @@ public class OutboundMessageQueueTest public static void init() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); } @Test diff --git a/test/unit/org/apache/cassandra/net/ProxyHandlerConnectionsTest.java b/test/unit/org/apache/cassandra/net/ProxyHandlerConnectionsTest.java index b21de607b3..47669e192e 100644 --- a/test/unit/org/apache/cassandra/net/ProxyHandlerConnectionsTest.java +++ b/test/unit/org/apache/cassandra/net/ProxyHandlerConnectionsTest.java @@ -37,6 +37,7 @@ import org.junit.BeforeClass; import org.junit.Test; import io.netty.buffer.ByteBuf; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionManager; @@ -85,6 +86,7 @@ public class ProxyHandlerConnectionsTest DatabaseDescriptor.daemonInitialization(); // call these to initialize everything in case a message is dropped, otherwise we will NPE in the commitlog CommitLog.instance.start(); + ServerTestUtils.initCMS(); CompactionManager.instance.getPendingTasks(); } diff --git a/test/unit/org/apache/cassandra/net/StartupClusterConnectivityCheckerTest.java b/test/unit/org/apache/cassandra/net/StartupClusterConnectivityCheckerTest.java index 0785f277da..80442d514c 100644 --- a/test/unit/org/apache/cassandra/net/StartupClusterConnectivityCheckerTest.java +++ b/test/unit/org/apache/cassandra/net/StartupClusterConnectivityCheckerTest.java @@ -32,10 +32,12 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.HeartBeatState; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.FBUtilities; public class StartupClusterConnectivityCheckerTest @@ -68,6 +70,7 @@ public class StartupClusterConnectivityCheckerTest public static void before() { DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); } @Before @@ -234,6 +237,7 @@ public class StartupClusterConnectivityCheckerTest if (processConnectAck) { Message msgIn = Message.builder(Verb.REQUEST_RSP, message.payload) + .withEpoch(Epoch.EMPTY) .from(to) .build(); MessagingService.instance().callbacks.get(message.id(), to).callback.onResponse(msgIn); diff --git a/test/unit/org/apache/cassandra/repair/FuzzTestBase.java b/test/unit/org/apache/cassandra/repair/FuzzTestBase.java index fb7702cd82..6c676b6938 100644 --- a/test/unit/org/apache/cassandra/repair/FuzzTestBase.java +++ b/test/unit/org/apache/cassandra/repair/FuzzTestBase.java @@ -50,8 +50,6 @@ import javax.annotation.Nullable; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; -import org.apache.cassandra.config.UnitConfigOverride; -import org.junit.Before; import org.junit.BeforeClass; import accord.utils.DefaultRandom; @@ -60,6 +58,7 @@ import accord.utils.Gens; import accord.utils.RandomSource; import org.agrona.collections.Long2ObjectHashMap; import org.agrona.collections.LongHashSet; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.concurrent.ExecutorBuilder; import org.apache.cassandra.concurrent.ExecutorBuilderFactory; import org.apache.cassandra.concurrent.ExecutorFactory; @@ -71,6 +70,7 @@ import org.apache.cassandra.concurrent.SequentialExecutorPlus; import org.apache.cassandra.concurrent.SimulatedExecutorFactory; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.UnitConfigOverride; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Digest; @@ -82,6 +82,7 @@ import org.apache.cassandra.db.repair.PendingAntiCompaction; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.gms.ApplicationState; import org.apache.cassandra.gms.EndpointState; @@ -96,7 +97,6 @@ import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.LocalStrategy; import org.apache.cassandra.locator.RangesAtEndpoint; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.net.ConnectionType; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; @@ -126,6 +126,7 @@ import org.apache.cassandra.streaming.StreamSession; import org.apache.cassandra.streaming.StreamState; import org.apache.cassandra.streaming.StreamingChannel; import org.apache.cassandra.streaming.StreamingDataInputPlus; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tools.nodetool.Repair; import org.apache.cassandra.utils.AbstractTypeGenerators; import org.apache.cassandra.utils.CassandraGenerators; @@ -269,11 +270,8 @@ public abstract class FuzzTestBase extends CQLTester.InMemory InMemory.setUpClass(); } - @Before - public void setupSchema() + public static void setupSchema() { - if (SETUP_SCHEMA) return; - SETUP_SCHEMA = true; // StorageService can not be mocked out, nor can ColumnFamilyStores, so make sure that the keyspace is a "local" keyspace to avoid replication as the peers don't actually exist for replication schemaChange(String.format("CREATE KEYSPACE %s WITH REPLICATION = {'class': '%s'}", SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, HackStrat.class.getName())); for (TableMetadata table : SystemDistributedKeyspace.metadata().tables) @@ -288,7 +286,7 @@ public abstract class FuzzTestBase extends CQLTester.InMemory execute(String.format("TRUNCATE %s.%s", SchemaConstants.SYSTEM_KEYSPACE_NAME, table)); } - private void createSchema() + private static void createSchema() { // The main reason to use random here with a fixed seed is just to have a set of tables that are not hard coded. // The tables will have diversity to them that most likely doesn't matter to repair (hence why the tables are shared), but @@ -704,15 +702,14 @@ public abstract class FuzzTestBase extends CQLTester.InMemory nodes.put(addressAndPort, node); } this.nodes = nodes; - - TokenMetadata tm = StorageService.instance.getTokenMetadata(); - tm.clearUnsafe(); + ServerTestUtils.recreateCMS(); + assert ClusterMetadata.current().directory.isEmpty() : ClusterMetadata.current().directory; for (Node inst : nodes.values()) { - tm.updateHostId(inst.hostId(), inst.broadcastAddressAndPort()); - for (Token token : inst.tokens()) - tm.updateNormalToken(token, inst.broadcastAddressAndPort()); + ClusterMetadataTestHelper.register(inst.broadcastAddressAndPort()); + ClusterMetadataTestHelper.join(inst.broadcastAddressAndPort(), inst.tokens()); } + setupSchema(); } public Closeable addListener(MessageListener listener) @@ -1227,9 +1224,9 @@ public abstract class FuzzTestBase extends CQLTester.InMemory public static class HackStrat extends LocalStrategy { - public HackStrat(String keyspaceName, TokenMetadata tokenMetadata, IEndpointSnitch snitch, Map configOptions) + public HackStrat(String keyspaceName, Map configOptions) { - super(keyspaceName, tokenMetadata, snitch, configOptions); + super(keyspaceName, configOptions); } } @@ -1285,9 +1282,15 @@ public abstract class FuzzTestBase extends CQLTester.InMemory next = it.next(); } if (FuzzTestBase.class.getName().equals(next.getClassName())) return Access.MAIN_THREAD_ONLY; - if (next.getClassName().startsWith("org.apache.cassandra.db.") || next.getClassName().startsWith("org.apache.cassandra.gms.") || next.getClassName().startsWith("org.apache.cassandra.cql3.") || next.getClassName().startsWith("org.apache.cassandra.metrics.") || next.getClassName().startsWith("org.apache.cassandra.utils.concurrent.") - || next.getClassName().startsWith("org.apache.cassandra.utils.TimeUUID") // this would be good to solve - || next.getClassName().startsWith(PendingAntiCompaction.class.getName())) + if (next.getClassName().startsWith("org.apache.cassandra.db.") || + next.getClassName().startsWith("org.apache.cassandra.gms.") || + next.getClassName().startsWith("org.apache.cassandra.cql3.") || + next.getClassName().startsWith("org.apache.cassandra.metrics.") || + next.getClassName().startsWith("org.apache.cassandra.utils.concurrent.") || + next.getClassName().startsWith("org.apache.cassandra.tcm") || + next.getClassName().startsWith("org.apache.cassandra.utils.TimeUUID") || + next.getClassName().startsWith("org.apache.cassandra.schema") || + next.getClassName().startsWith(PendingAntiCompaction.class.getName())) return Access.IGNORE; if (next.getClassName().startsWith("org.apache.cassandra.repair") || ActiveRepairService.class.getName().startsWith(next.getClassName())) return Access.REJECT; diff --git a/test/unit/org/apache/cassandra/repair/RepairJobTest.java b/test/unit/org/apache/cassandra/repair/RepairJobTest.java index 641926ed8a..e2b7e78b0d 100644 --- a/test/unit/org/apache/cassandra/repair/RepairJobTest.java +++ b/test/unit/org/apache/cassandra/repair/RepairJobTest.java @@ -40,6 +40,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.apache.cassandra.repair.messages.SyncResponse; import org.apache.cassandra.repair.messages.ValidationResponse; +import org.apache.cassandra.config.DatabaseDescriptor; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Before; @@ -168,6 +169,9 @@ public class RepairJobTest public static void setupClass() throws UnknownHostException { SchemaLoader.prepareServer(); + // todo; tcm - we default to paxos v2, which implies running paxos repairs, in trunk we default to v1 which doesn't + // this test doesn't run them, so disable here for now + DatabaseDescriptor.setPaxosRepairEnabled(false); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, CF)); diff --git a/test/unit/org/apache/cassandra/repair/RepairMessageVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/repair/RepairMessageVerbHandlerOutOfRangeTest.java new file mode 100644 index 0000000000..4a98435fbb --- /dev/null +++ b/test/unit/org/apache/cassandra/repair/RepairMessageVerbHandlerOutOfRangeTest.java @@ -0,0 +1,273 @@ +/* + * 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.repair; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.repair.messages.PrepareMessage; +import org.apache.cassandra.repair.messages.RepairMessage; +import org.apache.cassandra.repair.messages.ValidationResponse; +import org.apache.cassandra.repair.messages.ValidationRequest; +import org.apache.cassandra.repair.state.ParticipateState; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.ActiveRepairService; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.utils.TimeUUID; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.*; +import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +public class RepairMessageVerbHandlerOutOfRangeTest +{ + + private static final String TEST_NAME = "repair_message_vh_test_"; + private static final String KEYSPACE = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + private static List tableIds; + + static + { + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + } + + @BeforeClass + public static void init() throws Exception + { + SchemaLoader.loadSchema(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.recreateCMS(); + SchemaLoader.schemaDefinition(TEST_NAME); + ClusterMetadataTestHelper.register(broadcastAddress); + ServerTestUtils.markCMS(); + StorageService.instance.unsafeSetInitialized(); + tableIds = Collections.singletonList(Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata().id); + } + + @Before + public void setup() throws Exception + { + ServerTestUtils.resetCMS(); + // All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 300 + // Initially, the local node has no tokens so when indivividual test set owned tokens or + // pending ranges for the local node, they're always in relation to this. + // e.g. test calls setLocalTokens(100, 300) the ring now looks like + // peer -> (min, 0], (100, 200], (300, 400] + // local -> (0, 100], (200, 300], (400, max] + // + // Pending ranges are set in test using start/end pairs. + // Ring is initialised: + // peer -> (min, max] + // local -> (,] + // e.g. test calls setPendingRanges(0, 100, 200, 300) + // the pending ranges for local would be calculated as: + // local -> (0, 100], (200, 300] + ClusterMetadataTestHelper.addEndpoint(node1, Lists.newArrayList(token(0), token(200), token(400))); + } + + /******************************************************************************* + * + * PrepareMessage handling tests. A prepare request contains the entire set of ranges to be repaired. + * Where subrange repairs are not being used (or potentially where the specified range intersects the + * ranges of multiple instances) asserting the ranges at this point could cause the PREPARE message to + * be rejected by some or all participants and so the parent repair fails. + * + ******************************************************************************/ + + @Test + public void testPrepareWithAllRequestedRangesWithinOwned() throws Exception + { + setLocalTokens(100); + PrepareMessage prepare = prepareMsg(generateRanges(10, 20)); + tryPrepareExpectingSuccess(prepare); + } + + @Test + public void testPrepareWithAllRequestedRangesOutsideOwned() throws Exception + { + setLocalTokens(100); + PrepareMessage prepare = prepareMsg(generateRanges(110, 120)); + tryPrepareExpectingSuccess(prepare); + } + + @Test + public void testPrepareWithSomeRequestedRangesOutsideOwned() throws Exception + { + setLocalTokens(100); + PrepareMessage prepare = prepareMsg(generateRanges(10, 20, 110, 120)); + tryPrepareExpectingSuccess(prepare); + } + + /******************************************************************************* + * + * ValidationRequest handling tests. The ranges in Validation requests _are_ verified + * as the repair coordinator should tailor the ranges for these to the specific endpoint. + * + ******************************************************************************/ + + @Test + public void testValidationRequestWithRequestedRangeWithinOwned() throws Exception + { + setLocalTokens(100); + ValidationRequest request = validationMsg(generateRange(10, 20)); + tryValidationExpectingSuccess(request, false); + } + + @Test + public void testValidationRequestWithRequestedRangeOutsideOwned() throws Exception + { + setLocalTokens(100); + ValidationRequest request = validationMsg(generateRange(110, 120)); + tryValidationExpectingFailure(request); + } + + @Test + public void testValidationRequestWithRequestedRangeOverlappingOwned() throws Exception + { + setLocalTokens(100); + ValidationRequest request = validationMsg(generateRange(10, 120)); + tryValidationExpectingFailure(request); + } + + private static void tryValidationExpectingFailure(ValidationRequest request) throws Exception + { + tryValidation(request, true, false); + } + + private static void tryValidationExpectingSuccess(ValidationRequest request, boolean isOutOfRange) throws Exception + { + tryValidation(request, isOutOfRange, true); + } + + private static void tryValidation(ValidationRequest request, boolean isOutOfRange, boolean expectSuccess) throws Exception + { + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + MessagingService.instance().outboundSink.clear(); + MessagingService.instance().inboundSink.clear(); + ListenableFuture messageSink = registerOutgoingMessageSink(Verb.REPAIR_RSP); + RepairMessageVerbHandler handler = new RepairMessageVerbHandler(SharedContext.Global.instance); + int messageId = randomInt(); + // message must be prepared first as validate checks it is registered. + PrepareMessage prepare = prepareMsg(request.desc.parentSessionId, request.desc.ranges); + ActiveRepairService.instance().register(new ParticipateState(SharedContext.Global.instance.clock(), node1, prepare)); + Message message = Message.builder(Verb.VALIDATION_REQ, (RepairMessage)request).from(node1).withId(messageId).build(); + handler.doVerb(message); + ClusterMetadataTestHelper.MessageDelivery response = messageSink.get(500, TimeUnit.MILLISECONDS); + if (expectSuccess) + { + assertEquals(Verb.VALIDATION_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertEquals(node1, response.to); + assertTrue(response.message.payload instanceof ValidationResponse); + ValidationResponse completion = (ValidationResponse) response.message.payload; + assertTrue(completion.success()); + assertEquals(startMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount()); + } + else + { + assertEquals(Verb.FAILURE_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertEquals(node1, response.to); + assertEquals(startMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount()); + } + } + + private static void tryPrepareExpectingSuccess(PrepareMessage prepare) throws Exception + { + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + MessagingService.instance().outboundSink.clear(); + MessagingService.instance().inboundSink.clear(); + ListenableFuture messageSink = registerOutgoingMessageSink(); + RepairMessageVerbHandler handler = new RepairMessageVerbHandler(SharedContext.Global.instance); + int messageId = randomInt(); + Message message = Message.builder(Verb.PREPARE_MSG, (RepairMessage)prepare).from(node1).withId(messageId).build(); + handler.doVerb(message); + + MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS); + assertEquals(Verb.REPAIR_RSP, response.message.verb()); + assertEquals(broadcastAddress, response.message.from()); + assertEquals(messageId, response.message.id()); + assertEquals(node1, response.to); + assertFalse(response.message.payload instanceof RequestFailureReason); + assertEquals(startMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount()); + } + + private static PrepareMessage prepareMsg(Collection> ranges) + { + return prepareMsg(uuid(), ranges); + } + private static PrepareMessage prepareMsg(TimeUUID parentRepairSession, Collection> ranges) + { + return new PrepareMessage(parentRepairSession, tableIds, ranges, false, ActiveRepairService.UNREPAIRED_SSTABLE, true, PreviewKind.NONE); + } + + private static ValidationRequest validationMsg(Range range) + { + TimeUUID parentId = uuid(); + List stores = tableIds.stream() + .map(Schema.instance::getColumnFamilyStoreInstance) + .collect(Collectors.toList()); + ActiveRepairService.instance().registerParentRepairSession(parentId, + node1, + stores, + Collections.singleton(range), + false, + ActiveRepairService.UNREPAIRED_SSTABLE, + true, + PreviewKind.NONE); + return new ValidationRequest(new RepairJobDesc(parentId, uuid(), KEYSPACE, TABLE, Collections.singleton(range)), + randomInt()); + } + + public static TimeUUID uuid() + { + return nextTimeUUID(); + } +} + diff --git a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorMessagingTest.java b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorMessagingTest.java index b9d859f3d7..a26aa17683 100644 --- a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorMessagingTest.java +++ b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorMessagingTest.java @@ -32,6 +32,7 @@ import com.google.common.collect.Lists; import org.apache.cassandra.repair.CoordinatedRepairResult; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.Promise; @@ -57,7 +58,6 @@ import org.apache.cassandra.repair.messages.FinalizePropose; import org.apache.cassandra.repair.messages.PrepareConsistentRequest; import org.apache.cassandra.repair.messages.PrepareConsistentResponse; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; diff --git a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionTest.java b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionTest.java index 1f91b99795..4b634bcedd 100644 --- a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionTest.java +++ b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionTest.java @@ -31,12 +31,14 @@ import java.util.function.Supplier; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.net.Verb; import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.repair.AbstractRepairTest; @@ -63,7 +65,11 @@ import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; public class CoordinatorSessionTest extends AbstractRepairTest { - + @BeforeClass + public static void beforeClass() + { + ClusterMetadataTestHelper.setInstanceForTest(); + } static CoordinatorSession.Builder createBuilder() { CoordinatorSession.Builder builder = CoordinatorSession.builder(SharedContext.Global.instance); diff --git a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionsTest.java b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionsTest.java index cd9dfba4ed..a2acec4ac5 100644 --- a/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionsTest.java +++ b/test/unit/org/apache/cassandra/repair/consistent/CoordinatorSessionsTest.java @@ -32,8 +32,8 @@ import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.repair.AbstractRepairTest; import org.apache.cassandra.repair.NoSuchRepairSessionException; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.repair.messages.FailSession; diff --git a/test/unit/org/apache/cassandra/repair/consistent/LocalSessionTest.java b/test/unit/org/apache/cassandra/repair/consistent/LocalSessionTest.java index e19f8abca9..80ea042965 100644 --- a/test/unit/org/apache/cassandra/repair/consistent/LocalSessionTest.java +++ b/test/unit/org/apache/cassandra/repair/consistent/LocalSessionTest.java @@ -49,8 +49,8 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.repair.AbstractRepairTest; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.repair.KeyspaceRepairManager; -import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; diff --git a/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java b/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java index 634fab3817..8e93641e91 100644 --- a/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java +++ b/test/unit/org/apache/cassandra/repair/messages/RepairMessageSerializationsTest.java @@ -36,6 +36,7 @@ import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; @@ -66,6 +67,7 @@ public class RepairMessageSerializationsTest { DatabaseDescriptor.daemonInitialization(); originalPartitioner = StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ClusterMetadataTestHelper.setInstanceForTest(); } @AfterClass diff --git a/test/unit/org/apache/cassandra/repair/messages/RepairMessageTest.java b/test/unit/org/apache/cassandra/repair/messages/RepairMessageTest.java index e1a6eec3f1..b01a9fcbbd 100644 --- a/test/unit/org/apache/cassandra/repair/messages/RepairMessageTest.java +++ b/test/unit/org/apache/cassandra/repair/messages/RepairMessageTest.java @@ -32,6 +32,8 @@ import org.apache.cassandra.net.MessageDelivery; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; import org.apache.cassandra.repair.SharedContext; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.apache.cassandra.utils.Backoff; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; @@ -60,7 +62,9 @@ public class RepairMessageTest static { - DatabaseDescriptor.clientInitialization(); + DatabaseDescriptor.daemonInitialization(); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); RepairMetrics.init(); } diff --git a/test/unit/org/apache/cassandra/schema/MigrationManagerDropKSTest.java b/test/unit/org/apache/cassandra/schema/DropKSTest.java similarity index 99% rename from test/unit/org/apache/cassandra/schema/MigrationManagerDropKSTest.java rename to test/unit/org/apache/cassandra/schema/DropKSTest.java index c045b556d6..d2b8d8abcc 100644 --- a/test/unit/org/apache/cassandra/schema/MigrationManagerDropKSTest.java +++ b/test/unit/org/apache/cassandra/schema/DropKSTest.java @@ -36,7 +36,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -public class MigrationManagerDropKSTest +public class DropKSTest { private static final String KEYSPACE1 = "keyspace1"; private static final String TABLE1 = "standard1"; diff --git a/test/unit/org/apache/cassandra/schema/MigrationCoordinatorTest.java b/test/unit/org/apache/cassandra/schema/MigrationCoordinatorTest.java deleted file mode 100644 index 3783320566..0000000000 --- a/test/unit/org/apache/cassandra/schema/MigrationCoordinatorTest.java +++ /dev/null @@ -1,443 +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.schema; - -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; - -import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; -import org.junit.Assert; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ImmediateExecutor; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Mutation; -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.InetAddressAndPort; -import org.apache.cassandra.metrics.MessagingMetrics; -import org.apache.cassandra.net.EndpointMessagingVersions; -import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.net.RequestCallback; -import org.apache.cassandra.net.Verb; -import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.Pair; -import org.apache.cassandra.utils.concurrent.WaitQueue; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.internal.creation.MockSettingsImpl; - -import static com.google.common.util.concurrent.Futures.getUnchecked; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class MigrationCoordinatorTest -{ - private static final Logger logger = LoggerFactory.getLogger(MigrationCoordinatorTest.class); - - private static final InetAddressAndPort EP1; - private static final InetAddressAndPort EP2; - private static final InetAddressAndPort EP3; - - private static final UUID LOCAL_VERSION = UUID.randomUUID(); - private static final UUID V1 = UUID.randomUUID(); - private static final UUID V2 = UUID.randomUUID(); - - private static final EndpointState validEndpointState = mock(EndpointState.class); - - static - { - try - { - EP1 = InetAddressAndPort.getByName("10.0.0.1"); - EP2 = InetAddressAndPort.getByName("10.0.0.2"); - EP3 = InetAddressAndPort.getByName("10.0.0.3"); - } - catch (UnknownHostException e) - { - throw new AssertionError(e); - } - - DatabaseDescriptor.daemonInitialization(); - - when(validEndpointState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue(FBUtilities.getReleaseVersionString(), 0)); - } - - private static class Wrapper - { - final Queue>>> requests = new LinkedList<>(); - final ScheduledExecutorService oneTimeExecutor = mock(ScheduledExecutorService.class); - final Gossiper gossiper = mock(Gossiper.class); - final Set mergedSchemasFrom = new HashSet<>(); - final EndpointMessagingVersions versions = mock(EndpointMessagingVersions.class); - final MessagingService messagingService = mock(MessagingService.class, new MockSettingsImpl<>().defaultAnswer(a -> { - throw new UnsupportedOperationException(); - }).useConstructor(true, versions, mock(MessagingMetrics.class))); - - UUID localSchemaVersion = LOCAL_VERSION; - - final MigrationCoordinator coordinator; - - private Wrapper() - { - this(3); - } - - private Wrapper(int maxOutstandingRequests) - { - when(oneTimeExecutor.scheduleWithFixedDelay(any(), anyLong(), anyLong(), any())).thenAnswer(a -> { - a.getArgument(0, Runnable.class).run(); - return mock(ScheduledFuture.class); - }); - when(gossiper.getEndpointStateForEndpoint(any())).thenReturn(validEndpointState); - when(gossiper.isAlive(any())).thenReturn(true); - doAnswer(a -> requests.add(Pair.create(a.getArgument(1, InetAddressAndPort.class), a.getArgument(2, RequestCallback.class)))) - .when(messagingService) - .sendWithCallback(any(), any(), any()); - - when(versions.knows(any())).thenReturn(true); - when(versions.getRaw(any())).thenReturn(MessagingService.current_version); - this.coordinator = new MigrationCoordinator(messagingService, - ImmediateExecutor.INSTANCE, - oneTimeExecutor, - maxOutstandingRequests, - gossiper, - () -> localSchemaVersion, - (endpoint, ignored) -> mergedSchemasFrom.add(endpoint)); - } - - private InetAddressAndPort configureMocksForEndpoint(String endpointName, EndpointState es, Integer msgVersion, boolean gossipOnlyMember) throws UnknownHostException - { - InetAddressAndPort endpoint = InetAddressAndPort.getByName(endpointName); - return configureMocksForEndpoint(endpoint, es, msgVersion, gossipOnlyMember); - } - - private InetAddressAndPort configureMocksForEndpoint(InetAddressAndPort endpoint, EndpointState es, Integer msgVersion, boolean gossipOnlyMember) throws UnknownHostException - { - when(gossiper.getEndpointStateForEndpoint(endpoint)).thenReturn(es); - if (msgVersion == null) - { - when(versions.knows(endpoint)).thenReturn(false); - when(versions.getRaw(endpoint)).thenThrow(new IllegalArgumentException()); - } - else - { - when(versions.knows(endpoint)).thenReturn(true); - when(versions.getRaw(endpoint)).thenReturn(msgVersion); - } - when(gossiper.isGossipOnlyMember(endpoint)).thenReturn(gossipOnlyMember); - when(gossiper.isAlive(endpoint)).thenReturn(true); - - return endpoint; - } - } - - @Test - public void requestResponseCycle() throws InterruptedException - { - Wrapper wrapper = new Wrapper(1); - MigrationCoordinator coordinator = wrapper.coordinator; - - Assert.assertTrue(wrapper.requests.isEmpty()); - - // first schema report should send a migration request - getUnchecked(coordinator.reportEndpointVersion(EP1, V1)); - Assert.assertEquals(1, wrapper.requests.size()); - Assert.assertFalse(coordinator.awaitSchemaRequests(1)); - - // second should not - getUnchecked(coordinator.reportEndpointVersion(EP2, V1)); - Assert.assertEquals(1, wrapper.requests.size()); - Assert.assertFalse(coordinator.awaitSchemaRequests(1)); - - // until the first request fails, then the second endpoint should be contacted - Pair>> request1 = wrapper.requests.poll(); - Assert.assertEquals(EP1, request1.left); - request1.right.onFailure(null, null); - Assert.assertTrue(wrapper.mergedSchemasFrom.isEmpty()); - Assert.assertFalse(coordinator.awaitSchemaRequests(1)); - - // ... then the second endpoint should be contacted - Assert.assertEquals(1, wrapper.requests.size()); - Pair>> request2 = wrapper.requests.poll(); - Assert.assertEquals(EP2, request2.left); - Assert.assertFalse(coordinator.awaitSchemaRequests(1)); - request2.right.onResponse(Message.remoteResponse(request2.left, Verb.SCHEMA_PULL_RSP, Collections.emptyList())); - Assert.assertEquals(EP2, Iterables.getOnlyElement(wrapper.mergedSchemasFrom)); - Assert.assertTrue(coordinator.awaitSchemaRequests(1)); - - // and migration tasks should not be sent out for subsequent version reports - getUnchecked(coordinator.reportEndpointVersion(EP3, V1)); - Assert.assertTrue(wrapper.requests.isEmpty()); - } - - /** - * If we don't send a request for a version, and endpoints associated with - * it all change versions, we should signal anyone waiting on that version - */ - @Test - public void versionsAreSignaledWhenDeleted() - { - Wrapper wrapper = new Wrapper(); - - wrapper.coordinator.reportEndpointVersion(EP1, V1); - WaitQueue.Signal signal = wrapper.coordinator.getVersionInfoUnsafe(V1).register(); - Assert.assertFalse(signal.isSignalled()); - - wrapper.coordinator.reportEndpointVersion(EP1, V2); - Assert.assertNull(wrapper.coordinator.getVersionInfoUnsafe(V1)); - - Assert.assertTrue(signal.isSignalled()); - } - - /** - * If an endpoint is removed and no other endpoints are reporting its - * schema version, the version should be removed and we should signal - * anyone waiting on that version - */ - @Test - public void versionsAreSignaledWhenEndpointsRemoved() - { - Wrapper wrapper = new Wrapper(); - - wrapper.coordinator.reportEndpointVersion(EP1, V1); - WaitQueue.Signal signal = wrapper.coordinator.getVersionInfoUnsafe(V1).register(); - Assert.assertFalse(signal.isSignalled()); - - wrapper.coordinator.removeAndIgnoreEndpoint(EP1); - Assert.assertNull(wrapper.coordinator.getVersionInfoUnsafe(V1)); - - Assert.assertTrue(signal.isSignalled()); - } - - - private static void assertNoContact(Wrapper wrapper, InetAddressAndPort endpoint, UUID version, boolean startupShouldBeUnblocked) - { - Assert.assertTrue(wrapper.requests.isEmpty()); - Future future = wrapper.coordinator.reportEndpointVersion(EP1, V1); - if (future != null) - getUnchecked(future); - Assert.assertTrue(wrapper.requests.isEmpty()); - - Assert.assertEquals(startupShouldBeUnblocked, wrapper.coordinator.awaitSchemaRequests(1)); - } - - private static void assertNoContact(Wrapper coordinator, boolean startupShouldBeUnblocked) - { - assertNoContact(coordinator, EP1, V1, startupShouldBeUnblocked); - } - - @Test - public void dontContactNodesWithSameSchema() - { - Wrapper wrapper = new Wrapper(); - - wrapper.localSchemaVersion = V1; - assertNoContact(wrapper, true); - } - - @Test - public void dontContactIncompatibleNodes() - { - Wrapper wrapper = new Wrapper(); - - when(wrapper.gossiper.getEndpointStateForEndpoint(any())).thenReturn(null); // shouldPullFromEndpoint should return false in this case - assertNoContact(wrapper, false); - } - - @Test - public void dontContactDeadNodes() - { - Wrapper wrapper = new Wrapper(); - - when(wrapper.gossiper.isAlive(ArgumentMatchers.eq(EP1))).thenReturn(false); - assertNoContact(wrapper, EP1, V1, false); - } - - /** - * If a node has become incompatible between when the task was scheduled and when it - * was run, we should detect that and fail the task - */ - @Test - public void testGossipRace() - { - Wrapper wrapper = new Wrapper(); - when(wrapper.gossiper.getEndpointStateForEndpoint(any())).thenReturn(validEndpointState, (EndpointState) null); - - assertNoContact(wrapper, EP1, V1, false); - } - - @Test - public void testWeKeepSendingRequests() throws Exception - { - Wrapper wrapper = new Wrapper(); - - getUnchecked(wrapper.coordinator.reportEndpointVersion(EP3, V2)); - Pair>> cb = wrapper.requests.remove(); - cb.right.onResponse(Message.remoteResponse(cb.left, Verb.SCHEMA_PULL_RSP, Collections.emptyList())); - - getUnchecked(wrapper.coordinator.reportEndpointVersion(EP1, V1)); - getUnchecked(wrapper.coordinator.reportEndpointVersion(EP2, V1)); - - Pair>> prev = null; - Set EPs = Sets.newHashSet(EP1, EP2); - int ep1requests = 0; - int ep2requests = 0; - - for (int i = 0; i < 10; i++) - { - Assert.assertEquals(String.format("%s", i), 2, wrapper.requests.size()); - - Pair>> next = wrapper.requests.remove(); - - // we should be contacting endpoints in a round robin fashion - Assert.assertTrue(EPs.contains(next.left)); - if (prev != null && prev.left.equals(next.left)) - Assert.fail(String.format("Not expecting prev %s to be equal to next %s", prev.left, next.left)); - - // should send a new request - next.right.onFailure(null, null); - prev = next; - Assert.assertFalse(wrapper.coordinator.awaitSchemaRequests(1)); - - Assert.assertEquals(2, wrapper.requests.size()); - } - logger.info("{} -> {}", EP1, ep1requests); - logger.info("{} -> {}", EP2, ep2requests); - - // a single success should unblock startup though - cb = wrapper.requests.remove(); - cb.right.onResponse(Message.remoteResponse(cb.left, Verb.SCHEMA_PULL_RSP, Collections.emptyList())); - Assert.assertTrue(wrapper.coordinator.awaitSchemaRequests(1)); - } - - /** - * Pull unreceived schemas should detect and send requests out for any - * schemas that are marked unreceived and have no outstanding requests - */ - @Test - public void pullUnreceived() - { - Wrapper wrapper = new Wrapper(); - - when(wrapper.gossiper.getEndpointStateForEndpoint(any())).thenReturn(null); // shouldPullFromEndpoint should return false in this case - assertNoContact(wrapper, false); - - when(wrapper.gossiper.getEndpointStateForEndpoint(any())).thenReturn(validEndpointState); - Assert.assertEquals(0, wrapper.requests.size()); - wrapper.coordinator.start(); - Assert.assertEquals(1, wrapper.requests.size()); - } - - @Test - public void pushSchemaMutationsOnlyToViableNodes() throws UnknownHostException - { - Wrapper wrapper = new Wrapper(); - Collection mutations = Arrays.asList(mock(Mutation.class)); - - EndpointState validState = mock(EndpointState.class); - - InetAddressAndPort thisNode = wrapper.configureMocksForEndpoint(FBUtilities.getBroadcastAddressAndPort(), validState, MessagingService.current_version, false); - InetAddressAndPort unkonwnNode = wrapper.configureMocksForEndpoint("10.0.0.1:8000", validState, null, false); - InetAddressAndPort invalidMessagingVersionNode = wrapper.configureMocksForEndpoint("10.0.0.2:8000", validState, MessagingService.VERSION_30, false); - InetAddressAndPort regularNode = wrapper.configureMocksForEndpoint("10.0.0.3:8000", validState, MessagingService.current_version, false); - - when(wrapper.gossiper.getLiveMembers()).thenReturn(Sets.newHashSet(thisNode, unkonwnNode, invalidMessagingVersionNode, regularNode)); - - ArgumentCaptor msg = ArgumentCaptor.forClass(Message.class); - ArgumentCaptor targetEndpoint = ArgumentCaptor.forClass(InetAddressAndPort.class); - doNothing().when(wrapper.messagingService).send(msg.capture(), targetEndpoint.capture()); - - Pair, Set> result = wrapper.coordinator.pushSchemaMutations(mutations); - assertThat(result.left()).containsExactlyInAnyOrder(regularNode); - assertThat(result.right()).containsExactlyInAnyOrder(thisNode, unkonwnNode, invalidMessagingVersionNode); - assertThat(msg.getAllValues()).hasSize(1); - assertThat(msg.getValue().payload).isEqualTo(mutations); - assertThat(msg.getValue().verb()).isEqualTo(Verb.SCHEMA_PUSH_REQ); - assertThat(targetEndpoint.getValue()).isEqualTo(regularNode); - } - - @Test - public void reset() throws UnknownHostException - { - Collection mutations = Arrays.asList(mock(Mutation.class)); - - Wrapper wrapper = new Wrapper(); - wrapper.localSchemaVersion = SchemaConstants.emptyVersion; - - EndpointState invalidVersionState = mock(EndpointState.class); - when(invalidVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue("3.0", 0)); - when(invalidVersionState.getSchemaVersion()).thenReturn(V1); - - EndpointState validVersionState = mock(EndpointState.class); - when(validVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue(FBUtilities.getReleaseVersionString(), 0)); - when(validVersionState.getSchemaVersion()).thenReturn(V2); - - EndpointState localVersionState = mock(EndpointState.class); - when(localVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue(FBUtilities.getReleaseVersionString(), 0)); - when(localVersionState.getSchemaVersion()).thenReturn(SchemaConstants.emptyVersion); - - // some nodes - InetAddressAndPort thisNode = wrapper.configureMocksForEndpoint(FBUtilities.getBroadcastAddressAndPort(), localVersionState, MessagingService.current_version, false); - InetAddressAndPort noStateNode = wrapper.configureMocksForEndpoint("10.0.0.1:8000", null, MessagingService.current_version, false); - InetAddressAndPort diffMajorVersionNode = wrapper.configureMocksForEndpoint("10.0.0.2:8000", invalidVersionState, MessagingService.current_version, false); - InetAddressAndPort unkonwnNode = wrapper.configureMocksForEndpoint("10.0.0.2:8000", validVersionState, null, false); - InetAddressAndPort invalidMessagingVersionNode = wrapper.configureMocksForEndpoint("10.0.0.3:8000", validVersionState, MessagingService.VERSION_30, false); - InetAddressAndPort gossipOnlyMemberNode = wrapper.configureMocksForEndpoint("10.0.0.4:8000", validVersionState, MessagingService.current_version, true); - InetAddressAndPort regularNode1 = wrapper.configureMocksForEndpoint("10.0.0.5:8000", validVersionState, MessagingService.current_version, false); - InetAddressAndPort regularNode2 = wrapper.configureMocksForEndpoint("10.0.0.6:8000", validVersionState, MessagingService.current_version, false); - Set nodes = new LinkedHashSet<>(Arrays.asList(thisNode, noStateNode, diffMajorVersionNode, unkonwnNode, invalidMessagingVersionNode, gossipOnlyMemberNode, regularNode1, regularNode2)); - when(wrapper.gossiper.getLiveMembers()).thenReturn(nodes); - doAnswer(a -> { - Message msg = a.getArgument(0, Message.class); - InetAddressAndPort endpoint = a.getArgument(1, InetAddressAndPort.class); - RequestCallback callback = a.getArgument(2, RequestCallback.class); - - assertThat(msg.verb()).isEqualTo(Verb.SCHEMA_PULL_REQ); - assertThat(endpoint).isEqualTo(regularNode1); - callback.onResponse(Message.remoteResponse(regularNode1, Verb.SCHEMA_PULL_RSP, mutations)); - return null; - }).when(wrapper.messagingService).sendWithCallback(any(Message.class), any(InetAddressAndPort.class), any(RequestCallback.class)); - wrapper.coordinator.reset(); - assertThat(wrapper.mergedSchemasFrom).anyMatch(ep -> regularNode1.equals(ep) || regularNode2.equals(ep)); - assertThat(wrapper.mergedSchemasFrom).hasSize(1); - } -} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/schema/MockSchema.java b/test/unit/org/apache/cassandra/schema/MockSchema.java index 5d8b7c1dc7..8ca112ed18 100644 --- a/test/unit/org/apache/cassandra/schema/MockSchema.java +++ b/test/unit/org/apache/cassandra/schema/MockSchema.java @@ -19,18 +19,22 @@ package org.apache.cassandra.schema; import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; +import javax.annotation.Nullable; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.ObjectUtils; + import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.BufferDecoratedKey; @@ -65,6 +69,7 @@ import org.apache.cassandra.io.util.FileHandle; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.Memory; import org.apache.cassandra.service.CacheService; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FilterFactory; import org.apache.cassandra.utils.Throwables; @@ -82,12 +87,6 @@ public class MockSchema return sstableIds.computeIfAbsent(idx, ignored -> sstableIdGenerator.get()); } - public static Collection sstableIdGenerators() - { - return Arrays.asList(new Object[]{ Util.newSeqGen() }, - new Object[]{ Util.newUUIDGen() }); - } - private static final File tempFile = temp("mocksegmentedfile"); static @@ -104,9 +103,16 @@ public class MockSchema { throw Throwables.throwAsUncheckedException(ex); } + + Schema.instance = new MockSchemaProvider(); + if (DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized()) + DatabaseDescriptor.createAllDirectories(); + } + private static final AtomicInteger id = new AtomicInteger(); - public static final Keyspace ks = Keyspace.mockKS(KeyspaceMetadata.create("mockks", KeyspaceParams.simpleTransient(1))); + private static final String ksname = "mockks"; + private static KeyspaceMetadata mockKS = KeyspaceMetadata.create(ksname, KeyspaceParams.simpleTransient(1)); public static final IndexSummary indexSummary; @@ -294,7 +300,7 @@ public class MockSchema public static ColumnFamilyStore newCFS() { - return newCFS(ks.getName()); + return newCFS(ksname); } public static ColumnFamilyStore newCFS(String ksname) @@ -304,7 +310,7 @@ public class MockSchema public static ColumnFamilyStore newCFS(Function options) { - return newCFS(ks.getName(), options); + return newCFS(ksname, options); } public static ColumnFamilyStore newCFS(String ksname, Function options) @@ -314,10 +320,14 @@ public class MockSchema public static ColumnFamilyStore newCFS(TableMetadata metadata) { - return new ColumnFamilyStore(ks, metadata.name, Util.newSeqGen(), new TableMetadataRef(metadata), new Directories(metadata), false, false, false); + Tables tables = mockKS.tables.getNullable(metadata.name) == null + ? mockKS.tables.with(metadata) + : mockKS.tables.withSwapped(metadata); + mockKS = mockKS.withSwapped(tables); + return new ColumnFamilyStore(new Keyspace(mockKS), metadata.name, Util.newSeqGen(), metadata, new Directories(metadata), false, false); } - public static TableMetadata newTableMetadata(String ksname) + private static TableMetadata newTableMetadata(String ksname) { return newTableMetadata(ksname, "mockcf" + (id.incrementAndGet())); } @@ -367,4 +377,134 @@ public class MockSchema FileUtils.deleteRecursive(new File(dir, child)); } } + + public static class MockSchemaProvider implements SchemaProvider + { + private final SchemaProvider originalSchemaProvider = Schema.instance; + + @Override + public Set getKeyspaces() + { + Set kss = new HashSet<>(originalSchemaProvider.getKeyspaces()); + kss.add(ksname); + return kss; + } + + @Override + public int getNumberOfTables() + { + return originalSchemaProvider.getNumberOfTables() + mockKS.tables.size(); + } + + @Override + public ClusterMetadata submit(SchemaTransformation transformation) + { + return originalSchemaProvider.submit(transformation); + } + + @Override + public Keyspaces localKeyspaces() + { + return originalSchemaProvider.localKeyspaces(); + } + + @Override + public Keyspaces distributedKeyspaces() + { + return originalSchemaProvider.distributedKeyspaces().with(mockKS); + } + + @Override + public Keyspaces distributedAndLocalKeyspaces() + { + return Keyspaces.NONE.with(localKeyspaces()).with(distributedKeyspaces()); + } + + @Override + public Keyspaces getUserKeyspaces() + { + return distributedKeyspaces().without(SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES); + } + + @Override + public void registerListener(SchemaChangeListener listener) + { + originalSchemaProvider.registerListener(listener); + } + + @Override + public void unregisterListener(SchemaChangeListener listener) + { + originalSchemaProvider.unregisterListener(listener); + } + + public SchemaChangeNotifier schemaChangeNotifier() + { + return originalSchemaProvider.schemaChangeNotifier(); + } + + @Override + public Optional getIndexMetadata(String keyspace, String index) + { + return originalSchemaProvider.getIndexMetadata(keyspace, index); + } + + @Override + public Iterable getTablesAndViews(String keyspaceName) + { + Preconditions.checkNotNull(keyspaceName); + KeyspaceMetadata ksm = ObjectUtils.getFirstNonNull(() -> distributedKeyspaces().getNullable(keyspaceName), + () -> localKeyspaces().getNullable(keyspaceName)); + Preconditions.checkNotNull(ksm, "Keyspace %s not found", keyspaceName); + return ksm.tablesAndViews(); + } + + @Nullable + @Override + public Keyspace getKeyspaceInstance(String keyspaceName) + { + if (isMockKS(keyspaceName)) + return new Keyspace(mockKS); + + return originalSchemaProvider.getKeyspaceInstance(keyspaceName); + } + + @Nullable + @Override + public KeyspaceMetadata getKeyspaceMetadata(String keyspaceName) + { + if (isMockKS(keyspaceName)) + return mockKS; + return originalSchemaProvider.getKeyspaceMetadata(keyspaceName); + } + + @Nullable + @Override + public TableMetadata getTableMetadata(TableId id) + { + if (mockKS.tables.containsTable(id)) + return mockKS.tables.getNullable(id); + return originalSchemaProvider.getTableMetadata(id); + } + + @Nullable + @Override + public TableMetadata getTableMetadata(String keyspace, String table) + { + if (isMockKS(keyspace) || mockKS.tables.stream().anyMatch(tm -> tm.name.equals(table))) + return mockKS.tables.getNullable(table); + return originalSchemaProvider.getTableMetadata(keyspace, table); + } + + @Override + public void saveSystemKeyspace() + { + originalSchemaProvider.saveSystemKeyspace(); + } + + private boolean isMockKS(String keyspaceName) + { + return keyspaceName.equals(ksname)|| keyspaceName.equals(mockKS.name) || mockKS.tables.stream().anyMatch(tm -> tm.keyspace.equals(keyspaceName)); + } + } } diff --git a/test/unit/org/apache/cassandra/schema/RemoveWithoutDroppingTest.java b/test/unit/org/apache/cassandra/schema/RemoveWithoutDroppingTest.java deleted file mode 100644 index 683e52f40f..0000000000 --- a/test/unit/org/apache/cassandra/schema/RemoveWithoutDroppingTest.java +++ /dev/null @@ -1,125 +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.schema; - -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.ServerTestUtils; -import org.apache.cassandra.cql3.CQLTester; -import org.apache.cassandra.db.ColumnFamilyStore; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult; -import org.mockito.Mockito; - -import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_UPDATE_HANDLER_FACTORY_CLASS; -import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; -import static org.assertj.core.api.Assertions.assertThat; - -public class RemoveWithoutDroppingTest -{ - static volatile boolean dropDataOverride = true; - - static final SchemaChangeListener listener = Mockito.mock(SchemaChangeListener.class); - - @BeforeClass - public static void beforeClass() - { - ServerTestUtils.daemonInitialization(); - - SCHEMA_UPDATE_HANDLER_FACTORY_CLASS.setString(TestSchemaUpdateHandlerFactory.class.getName()); - CQLTester.prepareServer(); - Schema.instance.registerListener(listener); - } - - @Before - public void before() - { - Mockito.reset(listener); - } - - public static void callbackOverride(BiConsumer updateSchemaCallback, SchemaTransformationResult result, boolean dropData) - { - updateSchemaCallback.accept(result, dropDataOverride); - } - - public static class TestSchemaUpdateHandlerFactory implements SchemaUpdateHandlerFactory - { - @Override - public SchemaUpdateHandler getSchemaUpdateHandler(boolean online, BiConsumer updateSchemaCallback) - { - return online - ? new DefaultSchemaUpdateHandler((result, dropData) -> callbackOverride(updateSchemaCallback, result, dropData)) - : new OfflineSchemaUpdateHandler((result, dropData) -> callbackOverride(updateSchemaCallback, result, dropData)); - } - } - - private void testRemoveKeyspace(String ks, String tab, boolean expectDropped) throws Throwable - { - executeInternal(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", ks)); - executeInternal(String.format("CREATE TABLE %s.%s (id INT PRIMARY KEY, v INT)", ks, tab)); - executeInternal(String.format("INSERT INTO %s.%s (id, v) VALUES (?, ?)", ks, tab), 1, 2); - executeInternal(String.format("INSERT INTO %s.%s (id, v) VALUES (?, ?)", ks, tab), 3, 4); - ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(ks, tab); - cfs.forceFlush(ColumnFamilyStore.FlushReason.UNIT_TESTS).get(); - - KeyspaceMetadata ksm = Schema.instance.getKeyspaceMetadata(ks); - TableMetadata tm = Schema.instance.getTableMetadata(ks, tab); - - List directories = cfs.getDirectories().getCFDirectories(); - Set filesBefore = directories.stream().flatMap(d -> Arrays.stream(d.tryList(f -> !f.isDirectory()))).collect(Collectors.toSet()); - assertThat(filesBefore).isNotEmpty(); - - executeInternal(String.format("DROP KEYSPACE %s", ks)); - - Set filesAfter = directories.stream().flatMap(d -> Arrays.stream(d.tryList(f -> !f.isDirectory()))).collect(Collectors.toSet()); - if (expectDropped) - assertThat(filesAfter).isEmpty(); - else - assertThat(filesAfter).hasSameElementsAs(filesBefore); - - Mockito.verify(listener).onDropTable(tm, expectDropped); - Mockito.verify(listener).onDropKeyspace(ksm, expectDropped); - } - - @Test - public void testRemoveWithoutDropping() throws Throwable - { - dropDataOverride = false; - String ks = "test_remove_without_dropping"; - String tab = "test_table"; - testRemoveKeyspace(ks, tab, false); - } - - @Test - public void testRemoveWithDropping() throws Throwable - { - dropDataOverride = true; - String ks = "test_remove_with_dropping"; - String tab = "test_table"; - testRemoveKeyspace(ks, tab, true); - } -} diff --git a/test/unit/org/apache/cassandra/schema/SchemaChangeDuringRangeMovementTest.java b/test/unit/org/apache/cassandra/schema/SchemaChangeDuringRangeMovementTest.java new file mode 100644 index 0000000000..7c7eb8fb22 --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/SchemaChangeDuringRangeMovementTest.java @@ -0,0 +1,254 @@ +/* + * 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.schema; + +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.transformations.AlterSchema; +import org.apache.cassandra.triggers.TriggersTest; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class SchemaChangeDuringRangeMovementTest extends CQLTester +{ + // at the moment, the detail of the specific LockedRanges doesn't matter, transformations + // which are rejected in the presence of locking are rejected whatever is actually locked + private static final LockedRanges.AffectedRanges toLock = + LockedRanges.AffectedRanges.singleton(ReplicationParams.simple(3), + new Range<>(DatabaseDescriptor.getPartitioner().getMinimumToken(), + DatabaseDescriptor.getPartitioner().getRandomToken())); + + @Test + public void testAlwaysPermittedChanges() throws Throwable + { + // Category of schema transformations should always be allowed if syntactically + // and semantically valid. The presence of inflight range movements is not relevant. + + // create/drop function + // create/drop aggregate + withAndWithoutLockedRanges(() -> { + String f = createFunction(KEYSPACE, + "double, double", + "CREATE OR REPLACE FUNCTION %s(state double, val double) " + + "RETURNS NULL ON NULL INPUT " + + "RETURNS double " + + "LANGUAGE java " + + "AS 'return 0.0;';"); + + String a = createAggregate(KEYSPACE, + "double", + "CREATE OR REPLACE AGGREGATE %s(double) " + + "SFUNC " + shortFunctionName(f) + " " + + "STYPE double " + + "INITCOND 0"); + + execute("DROP AGGREGATE " + a); + execute("DROP FUNCTION " + f); + }); + + + // create/alter/drop table + // create/drop index + withAndWithoutLockedRanges(() -> { + createTable(KEYSPACE, "CREATE TABLE %s (id int primary key, v1 text, v2 text)"); + alterTable("ALTER TABLE %s ADD v3 int;"); + String i = createIndex(KEYSPACE, "CREATE INDEX ON %s(v1)"); + dropIndex("DROP INDEX %s." + i); + dropTable("DROP TABLE %s"); + + }); + + // create/drop trigger + withAndWithoutLockedRanges(() -> { + String t = createTable(KEYSPACE, "CREATE TABLE %s (id int primary key, v1 text, v2 text)"); + execute(String.format("CREATE TRIGGER tr1 ON %s.%s USING '%s'", + KEYSPACE, t, TriggersTest.TestTrigger.class.getName())); + execute("DROP TRIGGER tr1 ON %s"); + dropTable("DROP TABLE %s"); + }); + + // create/alter/drop type + withAndWithoutLockedRanges(() -> { + String t = createType(KEYSPACE, "CREATE TYPE %s (a int)"); + execute(String.format("ALTER TYPE %s.%s ADD b int", KEYSPACE, t)); + execute(String.format("DROP TYPE %s.%s", KEYSPACE, t)); + }); + + // create/alter/drop view + withAndWithoutLockedRanges(() -> { + String t = createTable("CREATE TABLE %s (" + + "a int," + + "b int," + + "PRIMARY KEY (a, b))"); + // don't use CQLTester::createView here as it waits for the view to be built, + // but this won't happen as StorageService isn't initialised + execute(String.format("CREATE MATERIALIZED VIEW %s.v " + + "AS " + + "SELECT * FROM %s.%s " + + "WHERE a IS NOT NULL AND b IS NOT NULL " + + "PRIMARY KEY (b, a)", KEYSPACE, KEYSPACE, t)); + execute(String.format("DROP MATERIALIZED VIEW %s.v", KEYSPACE)); + execute(String.format("DROP TABLE %s.%s", KEYSPACE, t)); + }); + } + + @Test + public void testRestrictedChanges() throws Throwable + { + final String RF9_KS1 = "rf9_ks1"; + final String RF9_KS2 = "rf9_ks2"; + final String RF9_KS3 = "rf9_ks3"; + final String RF9_KS4 = "rf9_ks4"; + final String RF10_KS1 = "rf10_ks1"; + + // keyspace creation should be rejected if the same replication + // params are not already in use by other keyspaces + execute(String.format("CREATE KEYSPACE %s " + + "WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':9}", RF9_KS1)); + // now lock ranges + ClusterMetadata metadata = ClusterMetadataService.instance().commit(new LockRanges()); + assertFalse(metadata.lockedRanges.locked.isEmpty()); + + // creating a ks with an existing set of replication params is permitted + execute(String.format("CREATE KEYSPACE %s WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':9}", RF9_KS2)); + // but one with distinct replication settings is rejected + expectRejection("CREATE KEYSPACE %s WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':10}", RF10_KS1); + + // altering a keyspace is allowed, as long as the replication settings aren't modified + execute(String.format("ALTER KEYSPACE %s WITH DURABLE_WRITES = FALSE", RF9_KS1)); + expectRejection("ALTER KEYSPACE %s WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':1}", RF9_KS1); + + // dropping a keyspace is permitted as long as it isn't the only one with its replication settings + execute(String.format("DROP KEYSPACE %s", RF9_KS2)); + expectRejection("DROP KEYSPACE %s", RF9_KS1); + + // dropping multiple keyspaces in one transformation + execute(String.format("CREATE KEYSPACE %s " + + "WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':9}", RF9_KS2)); + execute(String.format("CREATE KEYSPACE %s " + + "WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':9}", RF9_KS3)); + execute(String.format("CREATE KEYSPACE %s " + + "WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':9}", RF9_KS4)); + + SchemaTransformation dropAllowed = (metadata_) -> metadata_.schema.getKeyspaces().without(RF9_KS4).without(RF9_KS3); + metadata = ClusterMetadataService.instance().commit(new AlterSchema(dropAllowed, Schema.instance)); + assertFalse(metadata.schema.getKeyspaces().containsKeyspace(RF9_KS4)); + assertFalse(metadata.schema.getKeyspaces().containsKeyspace(RF9_KS3)); + + try + { + SchemaTransformation dropRejected = (metadata_) -> metadata_.schema.getKeyspaces().without(RF9_KS2).without(RF9_KS1); + ClusterMetadataService.instance().commit(new AlterSchema(dropRejected, Schema.instance)); + fail("Expected exception"); + } + catch (IllegalStateException e) + { + // IllegalStateException because we're going directly to CMS here with a programmatically constructed + // SchemaTransformation, in most circumstances this would be done via CQL and InvalidRequestException thrown + assertTrue(e.getMessage().contains("The requested schema changes cannot be executed as they conflict with " + + "ongoing range movements.")); + assertTrue(e.getMessage().contains(RF9_KS1)); + assertTrue(e.getMessage().contains(RF9_KS2)); + } + + metadata = ClusterMetadata.current(); + assertTrue(metadata.schema.getKeyspaces().containsKeyspace(RF9_KS2)); + assertTrue(metadata.schema.getKeyspaces().containsKeyspace(RF9_KS1)); + } + + private void expectRejection(String query, String keyspace) throws Throwable + { + try + { + execute(String.format(query, keyspace)); + fail("Expected exception"); + } + catch (InvalidRequestException e) + { + assertTrue(e.getMessage().contains("The requested schema changes cannot be executed as they conflict with " + + "ongoing range movements. The changes for keyspaces [" + keyspace + + "] are blocked")); + } + } + + private interface TestActions + { + void perform() throws Throwable; + } + + private void withAndWithoutLockedRanges(TestActions actions) throws Throwable + { + // first verify without any locked ranges + ClusterMetadata metadata = ClusterMetadata.current(); + assertTrue(metadata.lockedRanges.locked.isEmpty()); + actions.perform(); + + metadata = ClusterMetadataService.instance().commit(new LockRanges()); + assertFalse(metadata.lockedRanges.locked.isEmpty()); + actions.perform(); + + metadata = ClusterMetadataService.instance().commit(new ClearLockedRanges()); + assertTrue(metadata.lockedRanges.locked.isEmpty()); + } + + + // Custom transforms to lock/unlock an arbitrary set of ranges to + // avoid having to actually initiate some range movement + private static class LockRanges implements Transformation + { + @Override + public Kind kind() + { + return Kind.CUSTOM; + } + + @Override + public Result execute(ClusterMetadata metadata) + { + LockedRanges newLocked = metadata.lockedRanges.lock(LockedRanges.keyFor(metadata.epoch), toLock); + return Transformation.success(metadata.transformer().with(newLocked), toLock); + } + } + + private static class ClearLockedRanges implements Transformation + { + @Override + public Kind kind() + { + return Kind.CUSTOM; + } + + @Override + public Result execute(ClusterMetadata metadata) + { + LockedRanges newLocked = LockedRanges.EMPTY; + return Transformation.success(metadata.transformer().with(newLocked), LockedRanges.AffectedRanges.EMPTY); + } + } +} diff --git a/test/unit/org/apache/cassandra/schema/MigrationManagerTest.java b/test/unit/org/apache/cassandra/schema/SchemaChangesTest.java similarity index 97% rename from test/unit/org/apache/cassandra/schema/MigrationManagerTest.java rename to test/unit/org/apache/cassandra/schema/SchemaChangesTest.java index 16ef782e44..7bf5a5bd05 100644 --- a/test/unit/org/apache/cassandra/schema/MigrationManagerTest.java +++ b/test/unit/org/apache/cassandra/schema/SchemaChangesTest.java @@ -41,6 +41,7 @@ import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.marshal.ByteType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; @@ -58,7 +59,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -public class MigrationManagerTest +public class SchemaChangesTest { private static final String KEYSPACE1 = "keyspace1"; private static final String KEYSPACE3 = "keyspace3"; @@ -477,14 +478,17 @@ public class MigrationManagerTest } @Test - public void testValidateNullKeyspace() throws Exception + public void testValidateNullKeyspace() { TableMetadata.Builder builder = TableMetadata.builder(null, TABLE1).addPartitionKeyColumn("partitionKey", BytesType.instance); - - TableMetadata table1 = builder.build(); - thrown.expect(ConfigurationException.class); - thrown.expectMessage(null + "." + TABLE1 + ": Keyspace name must not be empty"); - table1.validate(); + try + { + builder.build(); + } + catch (ConfigurationException e) + { + assertTrue(e.getMessage().contains(null + "." + TABLE1 + ": Keyspace name must not be empty")); + } } @Test @@ -519,7 +523,7 @@ public class MigrationManagerTest SchemaTransformation transformation = SchemaTransformations.updateSystemKeyspace(keyspace, 0); Keyspaces before = Keyspaces.none(); - Keyspaces after = transformation.apply(before); + Keyspaces after = transformation.apply(ClusterMetadataTestHelper.minimalForTesting(before)); Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before, after); assertTrue(diff.altered.isEmpty()); @@ -535,7 +539,7 @@ public class MigrationManagerTest SchemaTransformation transformation = SchemaTransformations.updateSystemKeyspace(keyspace, 0); Keyspaces before = Keyspaces.of(keyspace); - Keyspaces after = transformation.apply(before); + Keyspaces after = transformation.apply(ClusterMetadataTestHelper.minimalForTesting(before)); Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before, after); assertTrue(diff.isEmpty()); @@ -552,7 +556,7 @@ public class MigrationManagerTest SchemaTransformation transformation = SchemaTransformations.updateSystemKeyspace(keyspace1, 1); Keyspaces before = Keyspaces.of(keyspace0); - Keyspaces after = transformation.apply(before); + Keyspaces after = transformation.apply(ClusterMetadataTestHelper.minimalForTesting(before)); Keyspaces.KeyspacesDiff diff = Keyspaces.diff(before, after); assertTrue(diff.created.isEmpty()); diff --git a/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java b/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java index 320c4fbf8e..0a06a853ed 100644 --- a/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java +++ b/test/unit/org/apache/cassandra/schema/SchemaKeyspaceTest.java @@ -20,18 +20,10 @@ package org.apache.cassandra.schema; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; - import org.junit.Assert; import org.junit.Assume; import org.junit.BeforeClass; @@ -49,21 +41,15 @@ import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.net.NoPayload; -import org.apache.cassandra.net.RequestCallback; -import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import org.apache.cassandra.utils.FBUtilities; -import org.apache.cassandra.utils.concurrent.AsyncPromise; -import org.jboss.byteman.contrib.bmunit.BMRule; import org.jboss.byteman.contrib.bmunit.BMUnitRunner; import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; @RunWith(BMUnitRunner.class) public class SchemaKeyspaceTest @@ -82,62 +68,6 @@ public class SchemaKeyspaceTest MessagingService.instance().listen(); } - /** See CASSANDRA-16856/16996. Make sure schema pulls are synchronized to prevent concurrent schema pull/writes */ - @Test - @BMRule(name = "delay partition updates to schema tables", - targetClass = "CassandraTableWriteHandler", - targetMethod = "write", - action = "Thread.sleep(1000);", - targetLocation = "AT EXIT") - public void testNoVisiblePartialSchemaUpdates() throws Exception - { - String keyspace = "sandbox"; - ExecutorService pool = Executors.newFixedThreadPool(2); - - SchemaKeyspace.truncate(); // Make sure there's nothing but the create we're about to do - CyclicBarrier barrier = new CyclicBarrier(2); - - Future creation = pool.submit(() -> { - barrier.await(); - createTable(keyspace, "CREATE TABLE test (a text primary key, b int, c int)"); - return null; - }); - - Future> mutationsFromThread = pool.submit(() -> { - barrier.await(); - return Stream.generate(this::getSchemaMutations).filter(m -> !m.isEmpty()).findFirst().get(); - }); - - creation.get(); // make sure the creation is finished - - Collection mutationsFromConcurrentAccess = mutationsFromThread.get(); - Collection settledMutations = getSchemaMutations(); - - // If the worker thread picked up the creation at all, it should have the same modifications. - // In other words, we should see all modifications or none. - if (mutationsFromConcurrentAccess.size() == settledMutations.size()) - { - assertEquals(1, settledMutations.size()); - Mutation mutationFromConcurrentAccess = mutationsFromConcurrentAccess.iterator().next(); - Mutation settledMutation = settledMutations.iterator().next(); - - assertEquals("Read partial schema change!", - settledMutation.getTableIds(), mutationFromConcurrentAccess.getTableIds()); - } - - pool.shutdownNow(); - } - - private Collection getSchemaMutations() - { - AsyncPromise> p = new AsyncPromise<>(); - MessagingService.instance().sendWithCallback(Message.out(Verb.SCHEMA_PULL_REQ, NoPayload.noPayload), - FBUtilities.getBroadcastAddressAndPort(), - (RequestCallback>) msg -> p.setSuccess(msg.payload)); - p.syncUninterruptibly(); - return p.getNow(); - } - @Test public void testConversionsInverses() throws Exception { @@ -223,17 +153,15 @@ public class SchemaKeyspaceTest private static void updateTable(String keyspace, TableMetadata oldTable, TableMetadata newTable) { KeyspaceMetadata ksm = Schema.instance.getKeyspaceInstance(keyspace).getMetadata(); - Mutation mutation = SchemaKeyspace.makeUpdateTableMutation(ksm, oldTable, newTable, FBUtilities.timestampMicros()).build(); - SchemaTestUtil.mergeAndAnnounceLocally(Collections.singleton(mutation)); + ksm = ksm.withSwapped(ksm.tables.without(oldTable).with(newTable)); + SchemaTestUtil.addOrUpdateKeyspace(ksm); } private static void createTable(String keyspace, String cql) { TableMetadata table = CreateTableStatement.parse(cql, keyspace).build(); - KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1), Tables.of(table)); - Mutation mutation = SchemaKeyspace.makeCreateTableMutation(ksm, table, FBUtilities.timestampMicros()).build(); - SchemaTestUtil.mergeAndAnnounceLocally(Collections.singleton(mutation)); + SchemaTestUtil.addOrUpdateKeyspace(ksm); } private static void checkInverses(TableMetadata metadata) throws Exception diff --git a/test/unit/org/apache/cassandra/schema/SchemaTest.java b/test/unit/org/apache/cassandra/schema/SchemaTest.java index 81f409f477..d8f176cccd 100644 --- a/test/unit/org/apache/cassandra/schema/SchemaTest.java +++ b/test/unit/org/apache/cassandra/schema/SchemaTest.java @@ -18,207 +18,198 @@ */ package org.apache.cassandra.schema; -import java.io.IOException; -import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.UUID; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinTask; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.Mutation; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.utils.FBUtilities; -import org.awaitility.Awaitility; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.statements.schema.AlterSchemaStatement; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class SchemaTest { + private static final String KS_PREFIX = "schema_test_ks_"; + private static final String KS_ONE = KS_PREFIX + "1"; + private static final String KS_TWO = KS_PREFIX + "2"; + @BeforeClass public static void setup() { DatabaseDescriptor.daemonInitialization(); ServerTestUtils.prepareServer(); - Schema.instance.loadFromDisk(); } - @Test - public void testTransKsMigration() throws IOException + @Before + public void clearSchema() { - assertEquals(0, Schema.instance.distributedKeyspaces().size()); - - Gossiper.instance.start((int) (System.currentTimeMillis() / 1000)); - try - { - // add a few. - saveKeyspaces(); - Schema.instance.reloadSchemaAndAnnounceVersion(); - - assertNotNull(Schema.instance.getKeyspaceMetadata("ks0")); - assertNotNull(Schema.instance.getKeyspaceMetadata("ks1")); - - Schema.instance.transform(keyspaces -> keyspaces.without(Arrays.asList("ks0", "ks1"))); - - assertNull(Schema.instance.getKeyspaceMetadata("ks0")); - assertNull(Schema.instance.getKeyspaceMetadata("ks1")); - - saveKeyspaces(); - Schema.instance.reloadSchemaAndAnnounceVersion(); - - assertNotNull(Schema.instance.getKeyspaceMetadata("ks0")); - assertNotNull(Schema.instance.getKeyspaceMetadata("ks1")); - } - finally - { - Gossiper.instance.stop(); - } + SchemaTestUtil.dropKeyspaceIfExist(KS_ONE, true); + SchemaTestUtil.dropKeyspaceIfExist(KS_TWO, true); } @Test - public void testKeyspaceCreationWhenNotInitialized() { - Keyspace.unsetInitialized(); - try - { - SchemaTestUtil.addOrUpdateKeyspace(KeyspaceMetadata.create("test", KeyspaceParams.simple(1)), true); - assertNotNull(Schema.instance.getKeyspaceMetadata("test")); - assertNull(Schema.instance.getKeyspaceInstance("test")); - - SchemaTestUtil.dropKeyspaceIfExist("test", true); - assertNull(Schema.instance.getKeyspaceMetadata("test")); - assertNull(Schema.instance.getKeyspaceInstance("test")); - } - finally - { - Keyspace.setInitialized(); - } - - SchemaTestUtil.addOrUpdateKeyspace(KeyspaceMetadata.create("test", KeyspaceParams.simple(1)), true); - assertNotNull(Schema.instance.getKeyspaceMetadata("test")); - assertNotNull(Schema.instance.getKeyspaceInstance("test")); - - SchemaTestUtil.dropKeyspaceIfExist("test", true); - assertNull(Schema.instance.getKeyspaceMetadata("test")); - assertNull(Schema.instance.getKeyspaceInstance("test")); - } - - /** - * This test checks that the schema version is updated only after the entire schema change is processed. - * In particular, we expect that {@link Schema#getVersion()} returns stale schema version as it was before - * the change, but it does not block. On the other hand, {@link Schema#getDistributedSchemaBlocking()} should block - * until the schema change is processed and return the new schema version. - */ - @Test - public void testGettingSchemaVersion() + public void tablesInNewKeyspaceHaveCorrectEpoch() { - Duration timeout = Duration.ofMillis(DatabaseDescriptor.getReadRpcTimeout(TimeUnit.MILLISECONDS)); + Tables tables = Tables.of(TableMetadata.minimal(KS_ONE, "modified1"), + TableMetadata.minimal(KS_ONE, "modified2")); + KeyspaceMetadata ksm = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1), tables); + applyAndAssertTableMetadata((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm), true); + } - // the listener with a barrier will let us control the schema change process - CyclicBarrier barrier = new CyclicBarrier(2); - SchemaChangeListener listener = new SchemaChangeListener() - { - @Override - public void onCreateKeyspace(KeyspaceMetadata keyspace) + @Test + public void newTablesInExistingKeyspaceHaveCorrectEpoch() + { + // Create an empty keyspace + KeyspaceMetadata ksm = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); + + // Add two tables and verify that the resultant table metadata has the correct epoch + Tables tables = Tables.of(TableMetadata.minimal(KS_ONE, "modified1"), TableMetadata.minimal(KS_ONE, "modified2")); + KeyspaceMetadata updated = ksm.withSwapped(tables); + applyAndAssertTableMetadata((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(updated), true); + } + + @Test + public void newTablesInNonEmptyKeyspaceHaveCorrectEpoch() + { + Tables tables = Tables.of(TableMetadata.minimal(KS_ONE, "unmodified")); + KeyspaceMetadata ksm = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1), tables); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); + + // Add a second table and assert that its table metadata has the latest epoch, but that the + // metadata of the other table stays unmodified + KeyspaceMetadata updated = ksm.withSwapped(tables.with(TableMetadata.minimal(KS_ONE, "modified1"))); + applyAndAssertTableMetadata((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(updated)); + } + + @Test + public void createTableCQLSetsCorrectEpoch() + { + Tables tables = Tables.of(TableMetadata.minimal(KS_ONE, "unmodified")); + KeyspaceMetadata ksm = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1), tables); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); + + applyAndAssertTableMetadata(cql(KS_ONE, "CREATE TABLE %s.modified (k int PRIMARY KEY)")); + } + + @Test + public void createTablesInMultipleKeyspaces() + { + KeyspaceMetadata ksm1 = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1)); + KeyspaceMetadata ksm2 = KeyspaceMetadata.create(KS_TWO, KeyspaceParams.simple(1)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm1).withAddedOrUpdated(ksm2)); + + // Add two tables in each ks and verify that the resultant table metadata has the correct epoch + Tables tables1 = Tables.of(TableMetadata.minimal(KS_ONE, "modified1"), TableMetadata.minimal(KS_ONE, "modified2")); + KeyspaceMetadata updated1 = ksm1.withSwapped(tables1); + Tables tables2 = Tables.of(TableMetadata.minimal(KS_TWO, "modified1"), TableMetadata.minimal(KS_TWO, "modified2")); + KeyspaceMetadata updated2 = ksm2.withSwapped(tables2); + applyAndAssertTableMetadata((metadata) -> metadata.schema.getKeyspaces() + .withAddedOrUpdated(updated1) + .withAddedOrUpdated(updated2), + true); + } + + + @Test + public void createTablesInMultipleNonEmptyKeyspaces() + { + KeyspaceMetadata ksm1 = KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1)); + KeyspaceMetadata ksm2 = KeyspaceMetadata.create(KS_TWO, KeyspaceParams.simple(1)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm1).withAddedOrUpdated(ksm2)); + + // Add two tables in each ks and verify that the resultant table metadata has the correct epoch + Tables tables1 = Tables.of(TableMetadata.minimal(KS_ONE, "unmodified1"), TableMetadata.minimal(KS_ONE, "unmodified2")); + KeyspaceMetadata updated1 = ksm1.withSwapped(tables1); + Tables tables2 = Tables.of(TableMetadata.minimal(KS_TWO, "unmodified1"), TableMetadata.minimal(KS_TWO, "unmodified2")); + KeyspaceMetadata updated2 = ksm2.withSwapped(tables2); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(updated1).withAddedOrUpdated(updated2)); + + // Add a third table in one ks and assert that its table metadata has the latest epoch, but that the + // metadata of the all other tables stays unmodified + applyAndAssertTableMetadata(cql(KS_ONE, "CREATE TABLE %s.modified (k int PRIMARY KEY)")); + } + + @Test + public void alterTableAndVerifyEpoch() + { + SchemaTestUtil.addOrUpdateKeyspace(KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1)), true); + Schema.instance.submit(cql(KS_ONE, "CREATE TABLE %s.unmodified (k int PRIMARY KEY)")); + Schema.instance.submit(cql(KS_ONE, "CREATE TABLE %s.modified ( " + + "k int, " + + "c1 int, " + + "v1 text, " + + "v2 text, " + + "v3 text, " + + "v4 text," + + "PRIMARY KEY(k,c1))")); + + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified DROP v4")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified ADD v5 text")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified RENAME c1 TO c2")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified WITH comment = 'altered'")); + } + + @Test + public void alterTableMultipleKeyspacesAndVerifyEpoch() + { + SchemaTestUtil.addOrUpdateKeyspace(KeyspaceMetadata.create(KS_ONE, KeyspaceParams.simple(1)), true); + SchemaTestUtil.addOrUpdateKeyspace(KeyspaceMetadata.create(KS_TWO, KeyspaceParams.simple(1)), true); + Schema.instance.submit(cql(KS_ONE, "CREATE TABLE %s.unmodified (k int PRIMARY KEY)")); + Schema.instance.submit(cql(KS_TWO, "CREATE TABLE %s.unmodified (k int PRIMARY KEY)")); + Schema.instance.submit(cql(KS_ONE, "CREATE TABLE %s.modified ( " + + "k int, " + + "c1 int, " + + "v1 text, " + + "v2 text, " + + "v3 text, " + + "v4 text," + + "PRIMARY KEY(k, c1))")); + + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified DROP v4")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified ADD v5 text")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified RENAME c1 TO c2")); + applyAndAssertTableMetadata(cql(KS_ONE, "ALTER TABLE %s.modified WITH comment = 'altered'")); + } + + private void applyAndAssertTableMetadata(SchemaTransformation transformation) + { + applyAndAssertTableMetadata(transformation, false); + } + + private void applyAndAssertTableMetadata(SchemaTransformation transformation, boolean onlyModified) + { + Epoch before = ClusterMetadata.current().epoch; + Schema.instance.submit(transformation); + Epoch after = ClusterMetadata.current().epoch; + assertTrue(after.isDirectlyAfter(before)); + DistributedSchema schema = ClusterMetadata.current().schema; + Predicate modified = (tm) -> tm.name.startsWith("modified") && tm.epoch.is(after); + Predicate predicate = onlyModified + ? modified + : modified.or((tm) -> tm.name.startsWith("unmodified") && tm.epoch.isBefore(after)); + + schema.getKeyspaces().forEach(keyspace -> { + if (keyspace.name.startsWith(KS_PREFIX)) { - try - { - barrier.await(); - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - catch (BrokenBarrierException e) - { - throw new RuntimeException(e); - } + boolean containsUnmodified = keyspace.tables.stream().anyMatch(tm -> tm.name.startsWith("unmodified")); + assertEquals("Expected an unmodified table metadata but none found in " + keyspace.name, !onlyModified, containsUnmodified); + assertTrue(keyspace.tables.stream().allMatch(predicate)); } - }; - - String suffix = String.valueOf(System.currentTimeMillis()); - try - { - Schema.instance.registerListener(listener); - - // initial schema version - we will expect it not to change until the entire schema change is processed - UUID v0 = Schema.instance.getDistributedSchemaBlocking().getVersion(); - - // a multi-step schema transformation - SchemaTransformation transformation = schema -> { - schema = schema.withAddedOrReplaced(KeyspaceMetadata.create("test1" + suffix, KeyspaceParams.simple(1))); - schema = schema.withAddedOrReplaced(KeyspaceMetadata.create("test2" + suffix, KeyspaceParams.simple(1))); - schema = schema.withAddedOrReplaced(KeyspaceMetadata.create("test3" + suffix, KeyspaceParams.simple(1))); - return schema; - }; - - // schema change is executed async because it is expected to wait for the barrier - ForkJoinTask resultFuture = ForkJoinPool.commonPool().submit(() -> Schema.instance.transform(transformation, true)); - - // let the first statement execute - barrier.await(timeout.toMillis(), TimeUnit.MILLISECONDS); - Awaitility.await().atMost(timeout).until(() -> Schema.instance.distributedKeyspaces().containsKeyspace("test1" + suffix)); - assertThat(Schema.instance.getVersion()).isEqualTo(v0); // we should be able to get version unsafely, and it should return stale schema version - assertThat(resultFuture.isDone()).isFalse(); // the schema change should not be done yet - - // let's query the schema version safely - in particular, this task should not finish until the schema change is done - ForkJoinTask futureSchemaVersion = ForkJoinPool.commonPool().submit(() -> Schema.instance.getDistributedSchemaBlocking().getVersion()); - - // let the second statement execute - barrier.await(timeout.toMillis(), TimeUnit.MILLISECONDS); - Awaitility.await().atMost(timeout).until(() -> Schema.instance.distributedKeyspaces().containsKeyspace("test2" + suffix)); - assertThat(Schema.instance.getVersion()).isEqualTo(v0); // we should be able to get version unsafely, and it should return stale schema version - assertThat(resultFuture.isDone()).isFalse(); // the schema change should not be done yet - assertThat(futureSchemaVersion.isDone()).isFalse(); // the schema version should not be available yet - - // let the third statement execute - barrier.await(timeout.toMillis(), TimeUnit.MILLISECONDS); - Awaitility.await().atMost(timeout).until(() -> Schema.instance.distributedKeyspaces().containsKeyspace("test3" + suffix)); - - SchemaTransformation.SchemaTransformationResult result = resultFuture.get(timeout.toMillis(), TimeUnit.MILLISECONDS); // the schema change should be done shortly - assertThat(futureSchemaVersion.isDone()).isTrue(); // the schema version should be available now - UUID v1 = futureSchemaVersion.get(); - assertThat(v1).isNotEqualTo(v0); // the schema version should be updated - assertThat(result).isNotNull(); - assertThat(Schema.instance.getVersion()).isEqualTo(v1); - assertThat(result.after.getVersion()).isEqualTo(v1); - assertThat(result.before.getVersion()).isEqualTo(v0); - } - catch (BrokenBarrierException | TimeoutException | ExecutionException e) - { - throw new RuntimeException(e); - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - finally - { - Schema.instance.unregisterListener(listener); - barrier.reset(); - Schema.instance.transform(schema -> schema.without(Arrays.asList("test1" + suffix, "test2" + suffix, "test3" + suffix))); - } + }); } - private void saveKeyspaces() + private static AlterSchemaStatement cql(String keyspace, String cql) { - Collection mutations = Arrays.asList(SchemaKeyspace.makeCreateKeyspaceMutation(KeyspaceMetadata.create("ks0", KeyspaceParams.simple(3)), FBUtilities.timestampMicros()).build(), - SchemaKeyspace.makeCreateKeyspaceMutation(KeyspaceMetadata.create("ks1", KeyspaceParams.simple(3)), FBUtilities.timestampMicros()).build()); - SchemaKeyspace.applyChanges(mutations); + return (AlterSchemaStatement) QueryProcessor.parseStatement(String.format(cql, keyspace)) + .prepare(ClientState.forInternalCalls()); } } diff --git a/test/unit/org/apache/cassandra/schema/SchemaTestUtil.java b/test/unit/org/apache/cassandra/schema/SchemaTestUtil.java index b937d15e0c..9fb2aac882 100644 --- a/test/unit/org/apache/cassandra/schema/SchemaTestUtil.java +++ b/test/unit/org/apache/cassandra/schema/SchemaTestUtil.java @@ -18,22 +18,14 @@ package org.apache.cassandra.schema; -import java.util.Collection; import java.util.Collections; -import java.util.concurrent.TimeUnit; -import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.concurrent.Stage; -import org.apache.cassandra.db.Mutation; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.net.Message; -import org.apache.cassandra.utils.concurrent.Future; - -import static org.apache.cassandra.net.Verb.SCHEMA_PUSH_REQ; +import org.apache.cassandra.tcm.ClusterMetadata; public class SchemaTestUtil { @@ -41,13 +33,24 @@ public class SchemaTestUtil public static void announceNewKeyspace(KeyspaceMetadata ksm) throws ConfigurationException { - ksm.validate(); + ksm.validate(ClusterMetadata.current()); if (Schema.instance.getKeyspaceMetadata(ksm.name) != null) throw new AlreadyExistsException(ksm.name); logger.info("Create new Keyspace: {}", ksm); - Schema.instance.transform(schema -> schema.withAddedOrUpdated(ksm)); + Schema.instance.submit(new SchemaTransformation() + { + public Keyspaces apply(ClusterMetadata metadata) + { + return metadata.schema.getKeyspaces().withAddedOrUpdated(ksm); + } + + public String cql() + { + return "fake"; + } + }); } public static void announceNewTable(TableMetadata cfm) @@ -67,19 +70,19 @@ public class SchemaTestUtil throw new AlreadyExistsException(cfm.keyspace, cfm.name); logger.info("Create new table: {}", cfm); - Schema.instance.transform(schema -> schema.withAddedOrUpdated(ksm.withSwapped(ksm.tables.with(cfm)))); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm.withSwapped(ksm.tables.with(cfm)))); } static void announceKeyspaceUpdate(KeyspaceMetadata ksm) { - ksm.validate(); + ksm.validate(ClusterMetadata.current()); KeyspaceMetadata oldKsm = Schema.instance.getKeyspaceMetadata(ksm.name); if (oldKsm == null) throw new ConfigurationException(String.format("Cannot update non existing keyspace '%s'.", ksm.name)); logger.info("Update Keyspace '{}' From {} To {}", ksm.name, oldKsm, ksm); - Schema.instance.transform(schema -> schema.withAddedOrUpdated(ksm)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); } public static void announceTableUpdate(TableMetadata updated) @@ -94,7 +97,7 @@ public class SchemaTestUtil updated.validateCompatibility(current); logger.info("Update table '{}/{}' From {} To {}", current.keyspace, current.name, current, updated); - Schema.instance.transform(schema -> schema.withAddedOrUpdated(ksm.withSwapped(ksm.tables.withSwapped(updated)))); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm.withSwapped(ksm.tables.withSwapped(updated)))); } static void announceKeyspaceDrop(String ksName) @@ -104,12 +107,13 @@ public class SchemaTestUtil throw new ConfigurationException(String.format("Cannot drop non existing keyspace '%s'.", ksName)); logger.info("Drop Keyspace '{}'", oldKsm.name); - Schema.instance.transform(schema -> schema.without(ksName)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().without(ksName)); } public static SchemaTransformation dropTable(String ksName, String cfName) { - return schema -> { + return (metadata) -> { + Keyspaces schema = metadata.schema.getKeyspaces(); KeyspaceMetadata ksm = schema.getNullable(ksName); TableMetadata tm = ksm != null ? ksm.getTableOrViewNullable(cfName) : null; if (tm == null) @@ -122,25 +126,22 @@ public class SchemaTestUtil public static void announceTableDrop(String ksName, String cfName) { logger.info("Drop table '{}/{}'", ksName, cfName); - Schema.instance.transform(dropTable(ksName, cfName)); + Schema.instance.submit(dropTable(ksName, cfName)); } + public static void addOrUpdateKeyspace(KeyspaceMetadata ksm) + { + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); + } + + @Deprecated(since = "CEP-21") // TODO remove this public static void addOrUpdateKeyspace(KeyspaceMetadata ksm, boolean locally) { - Schema.instance.transform(current -> current.withAddedOrUpdated(ksm)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); } public static void dropKeyspaceIfExist(String ksName, boolean locally) { - Schema.instance.transform(current -> current.without(Collections.singletonList(ksName))); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().without(Collections.singletonList(ksName))); } - - public static void mergeAndAnnounceLocally(Collection schemaMutations) - { - SchemaPushVerbHandler.instance.doVerb(Message.out(SCHEMA_PUSH_REQ, schemaMutations)); - Future f = Stage.MIGRATION.submit(() -> {}); - Assert.assertTrue(f.awaitThrowUncheckedOnInterrupt(10, TimeUnit.SECONDS)); - f.rethrowIfFailed(); - } - } diff --git a/test/unit/org/apache/cassandra/schema/TableMetadataSerDeTest.java b/test/unit/org/apache/cassandra/schema/TableMetadataSerDeTest.java new file mode 100644 index 0000000000..a8714899f6 --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/TableMetadataSerDeTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.schema; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.test.TestBaseImpl; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeVersion; + +public class TableMetadataSerDeTest extends TestBaseImpl +{ + @Test + public void droppedColumnsTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .start()) + { + cluster.coordinator(1).execute("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};", + ConsistencyLevel.ALL); + + cluster.coordinator(1).execute("CREATE TABLE ks.tbl (pk int,\n" + + " ck int,\n" + + " v1 int,\n" + + " v2 text,\n" + + " v3 text,\n" + + " v4 text,\n" + + " v5 text,\n" + + " PRIMARY KEY (pk, ck))", + ConsistencyLevel.ALL); + + cluster.coordinator(1).execute("ALTER TABLE ks.tbl drop v1", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute("ALTER TABLE ks.tbl drop v2", + ConsistencyLevel.ALL); + cluster.coordinator(1).execute("ALTER TABLE ks.tbl drop v3", + ConsistencyLevel.ALL); + + cluster.get(1).runOnInstance(() -> { + TableMetadata tableMetadata = ClusterMetadata.current().schema.getKeyspaceMetadata("ks").getTableOrViewNullable("tbl"); + + ByteBuffer out = null; + try (DataOutputBuffer dob = new DataOutputBuffer()) + { + TableMetadata.serializer.serialize(tableMetadata, dob, NodeVersion.CURRENT_METADATA_VERSION); + out = dob.buffer(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + Assert.assertEquals(out.limit(), TableMetadata.serializer.serializedSize(tableMetadata, NodeVersion.CURRENT_METADATA_VERSION)); + TableMetadata rt = null; + try + { + rt = TableMetadata.serializer.deserialize(new DataInputBuffer(out, true), Types.builder().build(), UserFunctions.builder().build(), NodeVersion.CURRENT_METADATA_VERSION); + } + catch (IOException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + Assert.assertEquals(rt, tableMetadata); + }); + } + } + + @Test + public void reversedTypeTest() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .start()) + { + cluster.coordinator(1).execute("CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};", + ConsistencyLevel.ALL); + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE ks.tbl (pk int"); + int counter = 1; + List asc = new ArrayList<>(); + List desc = new ArrayList<>(); + for (CQL3Type.Native value : CQL3Type.Native.values()) + { + if (value == CQL3Type.Native.EMPTY || value == CQL3Type.Native.COUNTER || value == CQL3Type.Native.DURATION) + continue; + String name = "ck" + (counter++); + sb.append(", ").append(name).append(" ").append(value.toString()); + asc.add(name); + name = "ck" + (counter++); + sb.append(", ").append(name).append(" ").append(value.toString()); + desc.add(name); + } + for (CQL3Type.Native value : CQL3Type.Native.values()) + { + if (value == CQL3Type.Native.EMPTY || value == CQL3Type.Native.COUNTER) + continue; + sb.append(", static").append(counter++).append(" ").append(value.toString()).append(" static"); + } + for (CQL3Type.Native value : CQL3Type.Native.values()) + { + if (value == CQL3Type.Native.EMPTY || value == CQL3Type.Native.COUNTER) + continue; + sb.append(", regular").append(counter++).append(" ").append(value.toString()); + } + + sb.append(", primary key (pk"); + for (String s : asc) + sb.append(", ").append(s); + for (String s : desc) + sb.append(", ").append(s); + sb.append("))"); + sb.append(" WITH CLUSTERING ORDER BY ("); + for (int i = 0; i < asc.size(); i++) + { + if (i > 0) + sb.append(", "); + sb.append(asc.get(i)).append(" ASC"); + } + for (int i = 0; i < desc.size(); i++) + { + sb.append(", "); + sb.append(desc.get(i)).append(" DESC"); + } + sb.append(");"); + System.out.println("sb.toString() = " + sb.toString()); + cluster.coordinator(1).execute(sb.toString(), + ConsistencyLevel.ALL); + cluster.get(1).runOnInstance(() -> { + TableMetadata before = ClusterMetadata.current().schema.getKeyspaceMetadata("ks").getTableOrViewNullable("tbl"); + + ByteBuffer out = null; + try (DataOutputBuffer dob = new DataOutputBuffer()) + { + TableMetadata.serializer.serialize(before, dob, NodeVersion.CURRENT_METADATA_VERSION); + out = dob.buffer(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + Assert.assertEquals(out.limit(), TableMetadata.serializer.serializedSize(before, NodeVersion.CURRENT_METADATA_VERSION)); + TableMetadata after = null; + try + { + after = TableMetadata.serializer.deserialize(new DataInputBuffer(out, true), Types.builder().build(), UserFunctions.builder().build(), NodeVersion.CURRENT_METADATA_VERSION); + } + catch (IOException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + + Assert.assertEquals(Tables.diff(Tables.of(before), Tables.of(after)).toString(), + after, before); + }); + } + } + +} diff --git a/test/unit/org/apache/cassandra/schema/TableMetadataTest.java b/test/unit/org/apache/cassandra/schema/TableMetadataTest.java index 357ac013ca..1580f31636 100644 --- a/test/unit/org/apache/cassandra/schema/TableMetadataTest.java +++ b/test/unit/org/apache/cassandra/schema/TableMetadataTest.java @@ -47,6 +47,7 @@ public class TableMetadataTest CompositeType type1 = CompositeType.getInstance(UTF8Type.instance, UTF8Type.instance, UTF8Type.instance); TableMetadata metadata1 = TableMetadata.builder(keyspaceName, tableName) .addPartitionKeyColumn("key", type1) + .offline() .build(); assertEquals("('test:', 'composite!', 'type)')", metadata1.partitionKeyAsCQLLiteral(type1.decompose("test:", "composite!", "type)"))); @@ -56,6 +57,7 @@ public class TableMetadataTest IntegerType.instance); TableMetadata metadata2 = TableMetadata.builder(keyspaceName, tableName) .addPartitionKeyColumn("key", type2) + .offline() .build(); ByteBuffer tupleValue = TupleType.buildValue(new ByteBuffer[]{ FloatType.instance.decompose(0.33f), UTF8Type.instance.decompose("tuple test") }); @@ -64,6 +66,7 @@ public class TableMetadataTest // plain type TableMetadata metadata3 = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("key", UTF8Type.instance).build(); assertEquals("'non-composite test'", metadata3.partitionKeyAsCQLLiteral(UTF8Type.instance.decompose("non-composite test"))); @@ -79,12 +82,14 @@ public class TableMetadataTest // one partition key column, no clustering key metadata = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("key", UTF8Type.instance) .build(); assertEquals("'Test'", metadata.primaryKeyAsCQLLiteral(UTF8Type.instance.decompose("Test"), Clustering.EMPTY)); // two partition key columns, no clustering key metadata = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("k1", UTF8Type.instance) .addPartitionKeyColumn("k2", Int32Type.instance) .build(); @@ -94,6 +99,7 @@ public class TableMetadataTest // one partition key column, one clustering key column metadata = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("key", UTF8Type.instance) .addClusteringColumn("clustering", UTF8Type.instance) .build(); @@ -107,6 +113,7 @@ public class TableMetadataTest // one partition key column, two clustering key columns metadata = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("key", UTF8Type.instance) .addClusteringColumn("c1", UTF8Type.instance) .addClusteringColumn("c2", UTF8Type.instance) @@ -123,6 +130,7 @@ public class TableMetadataTest // two partition key columns, two clustering key columns CompositeType composite = CompositeType.getInstance(Int32Type.instance, BooleanType.instance); metadata = TableMetadata.builder(keyspaceName, tableName) + .offline() .addPartitionKeyColumn("k1", Int32Type.instance) .addPartitionKeyColumn("k2", BooleanType.instance) .addClusteringColumn("c1", UTF8Type.instance) diff --git a/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java b/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java index 3b7d0cd9a0..23cfa80bfc 100644 --- a/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java +++ b/test/unit/org/apache/cassandra/service/ActiveRepairServiceTest.java @@ -18,6 +18,7 @@ */ package org.apache.cassandra.service; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,15 +36,13 @@ import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; - -import org.apache.cassandra.utils.TimeUUID; -import org.apache.cassandra.utils.concurrent.Condition; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.Config; @@ -60,13 +59,21 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.streaming.PreviewKind; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.concurrent.Refs; +import static org.apache.cassandra.ServerTestUtils.*; +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; import static org.apache.cassandra.repair.messages.RepairOption.DATACENTERS_KEY; import static org.apache.cassandra.repair.messages.RepairOption.FORCE_REPAIR_KEY; import static org.apache.cassandra.repair.messages.RepairOption.HOSTS_KEY; @@ -89,38 +96,31 @@ public class ActiveRepairServiceTest public String cfname; public ColumnFamilyStore store; - public InetAddressAndPort LOCAL, REMOTE; - - private boolean initialized; + public static InetAddressAndPort LOCAL, REMOTE; @BeforeClass - public static void defineSchema() throws ConfigurationException + public static void defineSchema() throws ConfigurationException, UnknownHostException { - SchemaLoader.prepareServer(); - SchemaLoader.createKeyspace(KEYSPACE5, - KeyspaceParams.simple(2), - SchemaLoader.standardCFMD(KEYSPACE5, CF_COUNTER), - SchemaLoader.standardCFMD(KEYSPACE5, CF_STANDARD1)); + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.startGossiper(); } @Before public void prepare() throws Exception { - if (!initialized) - { - SchemaLoader.startGossiper(); - initialized = true; - - LOCAL = FBUtilities.getBroadcastAddressAndPort(); - // generate a fake endpoint for which we can spoof receiving/sending trees - REMOTE = InetAddressAndPort.getByName("127.0.0.2"); - } - - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.clearUnsafe(); - StorageService.instance.setTokens(Collections.singleton(tmd.partitioner.getRandomToken())); - tmd.updateNormalToken(tmd.partitioner.getMinimumToken(), REMOTE); - assert tmd.isMember(REMOTE); + resetCMS(); + SchemaLoader.createKeyspace(KEYSPACE5, + KeyspaceParams.simple(2), + SchemaLoader.standardCFMD(KEYSPACE5, CF_COUNTER), + SchemaLoader.standardCFMD(KEYSPACE5, CF_STANDARD1)); + LOCAL = FBUtilities.getBroadcastAddressAndPort(); + // generate a fake endpoint for which we can spoof receiving/sending trees + REMOTE = InetAddressAndPort.getByName("127.0.0.2"); + NodeId local = Register.register(new NodeAddresses(LOCAL)); + NodeId remote = Register.register(new NodeAddresses(REMOTE)); + UnsafeJoin.unsafeJoin(local, Collections.singleton(DatabaseDescriptor.getPartitioner().getRandomToken())); + UnsafeJoin.unsafeJoin(remote, Collections.singleton(DatabaseDescriptor.getPartitioner().getMinimumToken())); } @Test @@ -141,15 +141,14 @@ public class ActiveRepairServiceTest @Test public void testGetNeighborsTimesTwo() throws Throwable { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - // generate rf*2 nodes, and ensure that only neighbors specified by the ARS are returned addTokens(2 * Keyspace.open(KEYSPACE5).getReplicationStrategy().getReplicationFactor().allReplicas); + ClusterMetadata metadata = ClusterMetadata.current(); AbstractReplicationStrategy ars = Keyspace.open(KEYSPACE5).getReplicationStrategy(); Set expected = new HashSet<>(); - for (Replica replica : ars.getAddressReplicas().get(FBUtilities.getBroadcastAddressAndPort())) + for (Replica replica : ars.getAddressReplicas(metadata).get(FBUtilities.getBroadcastAddressAndPort())) { - expected.addAll(ars.getRangeAddresses(tmd.cloneOnlyTokenMap()).get(replica.range()).endpoints()); + expected.addAll(ars.getRangeAddresses(metadata).get(replica.range()).endpoints()); } expected.remove(FBUtilities.getBroadcastAddressAndPort()); Iterable> ranges = StorageService.instance.getLocalReplicas(KEYSPACE5).ranges(); @@ -164,14 +163,11 @@ public class ActiveRepairServiceTest @Test public void testGetNeighborsPlusOneInLocalDC() throws Throwable { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - // generate rf+1 nodes, and ensure that all nodes are returned Set expected = addTokens(1 + Keyspace.open(KEYSPACE5).getReplicationStrategy().getReplicationFactor().allReplicas); expected.remove(FBUtilities.getBroadcastAddressAndPort()); // remove remote endpoints - TokenMetadata.Topology topology = tmd.cloneOnlyTokenMap().getTopology(); - HashSet localEndpoints = Sets.newHashSet(topology.getDatacenterEndpoints().get(DatabaseDescriptor.getLocalDataCenter())); + HashSet localEndpoints = Sets.newHashSet(ClusterMetadata.current().directory.datacenterEndpoints(DatabaseDescriptor.getLocalDataCenter())); expected = Sets.intersection(expected, localEndpoints); Iterable> ranges = StorageService.instance.getLocalReplicas(KEYSPACE5).ranges(); @@ -186,20 +182,18 @@ public class ActiveRepairServiceTest @Test public void testGetNeighborsTimesTwoInLocalDC() throws Throwable { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - // generate rf*2 nodes, and ensure that only neighbors specified by the ARS are returned addTokens(2 * Keyspace.open(KEYSPACE5).getReplicationStrategy().getReplicationFactor().allReplicas); + ClusterMetadata metadata = ClusterMetadata.current(); AbstractReplicationStrategy ars = Keyspace.open(KEYSPACE5).getReplicationStrategy(); Set expected = new HashSet<>(); - for (Replica replica : ars.getAddressReplicas().get(FBUtilities.getBroadcastAddressAndPort())) + for (Replica replica : ars.getAddressReplicas(metadata).get(FBUtilities.getBroadcastAddressAndPort())) { - expected.addAll(ars.getRangeAddresses(tmd.cloneOnlyTokenMap()).get(replica.range()).endpoints()); + expected.addAll(ars.getRangeAddresses(metadata).get(replica.range()).endpoints()); } expected.remove(FBUtilities.getBroadcastAddressAndPort()); // remove remote endpoints - TokenMetadata.Topology topology = tmd.cloneOnlyTokenMap().getTopology(); - HashSet localEndpoints = Sets.newHashSet(topology.getDatacenterEndpoints().get(DatabaseDescriptor.getLocalDataCenter())); + HashSet localEndpoints = Sets.newHashSet(ClusterMetadata.current().directory.datacenterEndpoints(DatabaseDescriptor.getLocalDataCenter())); expected = Sets.intersection(expected, localEndpoints); Iterable> ranges = StorageService.instance.getLocalReplicas(KEYSPACE5).ranges(); @@ -214,15 +208,14 @@ public class ActiveRepairServiceTest @Test public void testGetNeighborsTimesTwoInSpecifiedHosts() throws Throwable { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - // generate rf*2 nodes, and ensure that only neighbors specified by the hosts are returned addTokens(2 * Keyspace.open(KEYSPACE5).getReplicationStrategy().getReplicationFactor().allReplicas); + ClusterMetadata metadata = ClusterMetadata.current(); AbstractReplicationStrategy ars = Keyspace.open(KEYSPACE5).getReplicationStrategy(); List expected = new ArrayList<>(); - for (Replica replicas : ars.getAddressReplicas().get(FBUtilities.getBroadcastAddressAndPort())) + for (Replica replicas : ars.getAddressReplicas(metadata).get(FBUtilities.getBroadcastAddressAndPort())) { - expected.addAll(ars.getRangeAddresses(tmd.cloneOnlyTokenMap()).get(replicas.range()).endpoints()); + expected.addAll(ars.getRangeAddresses(metadata).get(replicas.range()).endpoints()); } expected.remove(FBUtilities.getBroadcastAddressAndPort()); @@ -267,12 +260,15 @@ public class ActiveRepairServiceTest Set addTokens(int max) throws Throwable { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); Set endpoints = new HashSet<>(); for (int i = 1; i <= max; i++) { InetAddressAndPort endpoint = InetAddressAndPort.getByName("127.0.0." + i); - tmd.updateNormalToken(tmd.partitioner.getRandomToken(), endpoint); + if (ClusterMetadata.current().directory.peerId(endpoint) == null) + { + NodeId nodeId = Register.register(new NodeAddresses(endpoint)); + UnsafeJoin.unsafeJoin(nodeId, Collections.singleton(DatabaseDescriptor.getPartitioner().getRandomToken())); + } endpoints.add(endpoint); } return endpoints; diff --git a/test/unit/org/apache/cassandra/service/BootstrapTransientTest.java b/test/unit/org/apache/cassandra/service/BootstrapTransientTest.java index ea96765ab5..d6f4aa9a09 100644 --- a/test/unit/org/apache/cassandra/service/BootstrapTransientTest.java +++ b/test/unit/org/apache/cassandra/service/BootstrapTransientTest.java @@ -25,10 +25,8 @@ import java.util.Collection; import java.util.List; import com.google.common.collect.ImmutableMap; - -import org.apache.cassandra.locator.EndpointsByReplica; -import org.apache.cassandra.locator.EndpointsForRange; import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -37,15 +35,23 @@ import org.apache.cassandra.dht.OrderPreservingPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.RangeStreamer; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.AbstractEndpointSnitch; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.IEndpointSnitch; +import org.apache.cassandra.locator.EndpointsByReplica; +import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; -import org.apache.cassandra.locator.SimpleStrategy; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.MovementMap; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; import org.apache.cassandra.utils.Pair; import static org.apache.cassandra.locator.Replica.fullReplica; @@ -59,20 +65,51 @@ import static org.apache.cassandra.service.StorageServiceTest.assertMultimapEqua */ public class BootstrapTransientTest { + static final String KEYSPACE = "TestKeyspace"; static InetAddressAndPort address02; static InetAddressAndPort address03; static InetAddressAndPort address04; static InetAddressAndPort address05; + Token tenToken = new OrderPreservingPartitioner.StringToken("00010"); + Token twentyToken = new OrderPreservingPartitioner.StringToken("00020"); + Token thirtyToken = new OrderPreservingPartitioner.StringToken("00030"); + Token fourtyToken = new OrderPreservingPartitioner.StringToken("00040"); + + Range range30_10 = new Range<>(thirtyToken, tenToken); + Range range10_20 = new Range<>(tenToken, twentyToken); + Range range20_30 = new Range<>(twentyToken, thirtyToken); + Range range30_40 = new Range<>(thirtyToken, fourtyToken); + + RangesAtEndpoint toFetch = RangesAtEndpoint.of(new Replica(address05, range30_40, true), + new Replica(address05, range20_30, true), + new Replica(address05, range10_20, false)); + @BeforeClass public static void setUpClass() throws Exception { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(OrderPreservingPartitioner.instance); + DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); address02 = InetAddressAndPort.getByName("127.0.0.2"); address03 = InetAddressAndPort.getByName("127.0.0.3"); address04 = InetAddressAndPort.getByName("127.0.0.4"); address05 = InetAddressAndPort.getByName("127.0.0.5"); } + @Before + public void setup() + { + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); + ClusterMetadataTestHelper.addEndpoint(address02, range30_10.right); + ClusterMetadataTestHelper.addEndpoint(address03, range10_20.right); + ClusterMetadataTestHelper.addEndpoint(address04, range20_30.right); + KeyspaceMetadata ksm = KeyspaceMetadata.create(KEYSPACE, KeyspaceParams.simple("3/1")); + SchemaTestUtil.addOrUpdateKeyspace(ksm); + } + + private final List downNodes = new ArrayList<>(); final RangeStreamer.SourceFilter alivePredicate = new RangeStreamer.SourceFilter() @@ -116,28 +153,6 @@ public class BootstrapTransientTest sourceFilterDownNodes.clear(); } - @BeforeClass - public static void setupDD() - { - DatabaseDescriptor.daemonInitialization(); - } - - Token tenToken = new OrderPreservingPartitioner.StringToken("00010"); - Token twentyToken = new OrderPreservingPartitioner.StringToken("00020"); - Token thirtyToken = new OrderPreservingPartitioner.StringToken("00030"); - Token fourtyToken = new OrderPreservingPartitioner.StringToken("00040"); - - Range range30_10 = new Range<>(thirtyToken, tenToken); - Range range10_20 = new Range<>(tenToken, twentyToken); - Range range20_30 = new Range<>(twentyToken, thirtyToken); - Range range30_40 = new Range<>(thirtyToken, fourtyToken); - - RangesAtEndpoint toFetch = RangesAtEndpoint.of(new Replica(address05, range30_40, true), - new Replica(address05, range20_30, true), - new Replica(address05, range10_20, false)); - - - public EndpointsForRange endpoints(Replica... replicas) { assert replicas.length > 0; @@ -153,71 +168,72 @@ public class BootstrapTransientTest return builder.build(); } @Test - public void testRangeStreamerRangesToFetch() throws Exception + public void testRangeStreamerRangesToFetch() { EndpointsByReplica expectedResult = new EndpointsByReplica(ImmutableMap.of( - transientReplica(address05, range10_20), endpoints(transientReplica(address02, range10_20)), - fullReplica(address05, range20_30), endpoints(transientReplica(address03, range20_30), fullReplica(address04, range20_30)), - fullReplica(address05, range30_40), endpoints(transientReplica(address04, range30_10), fullReplica(address02, range30_10)))); + transientReplica(address05, range10_20), endpoints(transientReplica(address02, range10_20)), + fullReplica(address05, range20_30), endpoints(transientReplica(address03, range20_30), fullReplica(address02, range20_30)), + fullReplica(address05, range30_40), endpoints(transientReplica(address04, range30_40), fullReplica(address03, range30_40)))); - invokeCalculateRangesToFetchWithPreferredEndpoints(toFetch, constructTMDs(), expectedResult); - } + /* + * Pre-TCM expected result, there are a couple of differences worth explaining... + * + * transientReplica(address05, range10_20), endpoints(transientReplica(address02, range10_20)), + * fullReplica(address05, range20_30), endpoints(transientReplica(address03, range20_30), fullReplica(address04, range20_30)), + * fullReplica(address05, range30_40), endpoints(transientReplica(address04, range30_10), fullReplica(address02, range30_10)))); - private Pair constructTMDs() - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(range30_10.right, address02); - tmd.updateNormalToken(range10_20.right, address03); - tmd.updateNormalToken(range20_30.right, address04); - TokenMetadata updated = tmd.cloneOnlyTokenMap(); - updated.updateNormalToken(range30_40.right, address05); + * First, the ranges of the source replicas now exactly match the dest replicas. This is because we split + * existing ranges without modifying ownership at the prepare stage. + * + * Second, the full replicas of the ranges for which the new node is becoming a full replica are different from + * before. (20,30] used to be sourced from address04, but now comes from address02. Similarly, (30, 40] used to + * be sourced from address02, but now is taken from address03. The reason for this is that we now identify the + * strict sources slightly differently, but with semantically equivalent outcomes. + * + * We have 3/1 replicas for a given range with a new node is bootstrapping as a Full replica. This causes an + * existing Full to become Transient and the existing Transient to give up the range entirely. The old and new + * implementations work slightly differently in this case: + * + * Post CEP-21, the new node will pick the replica going from Full to Transient as the stream source. + * In earlier versions, the Transient replica giving up the range will be chosen initially, but this doesn't + * satisfy the `isSufficient` predicate (i.e. a Transient replica can't be the source for a Full one), so we + * pick the first existing Full replica for the range, which might not be the same one TCM picked. + * + * In the end, neither implementation can pick the replica giving up the range entirely (because it is Transient + * and the new replica is Full), despite strict consistency and so both pick an existing Full replica as the + * source. + */ - return Pair.create(tmd, updated); + NodeId newNode = ClusterMetadataTestHelper.register(address05); + ClusterMetadataTestHelper.JoinProcess join = ClusterMetadataTestHelper.lazyJoin(address05, range30_40.right); + join.prepareJoin(); + ClusterMetadata metadata = ClusterMetadata.current(); + BootstrapAndJoin joiningPlan = (BootstrapAndJoin) metadata.inProgressSequences.get(newNode); + invokeCalculateRangesToFetchWithPreferredEndpoints(toFetch, metadata, joiningPlan, expectedResult); } private void invokeCalculateRangesToFetchWithPreferredEndpoints(ReplicaCollection toFetch, - Pair tmds, + ClusterMetadata metadata, + BootstrapAndJoin joinPlan, EndpointsByReplica expectedResult) { DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - + Pair movements = joinPlan.getMovementMaps(metadata); EndpointsByReplica result = RangeStreamer.calculateRangesToFetchWithPreferredEndpoints((address, replicas) -> replicas, - simpleStrategy(tmds.left), - toFetch, + simpleStrategy(metadata), true, - tmds.left, - tmds.right, - "TestKeyspace", - sourceFilters); + metadata, + KEYSPACE, + sourceFilters, + movements.left, + movements.right); result.asMap().forEach((replica, list) -> System.out.printf("Replica %s, sources %s%n", replica, list)); assertMultimapEqualsIgnoreOrder(expectedResult, result); - } - private AbstractReplicationStrategy simpleStrategy(TokenMetadata tmd) + private AbstractReplicationStrategy simpleStrategy(ClusterMetadata metadata) { - IEndpointSnitch snitch = new AbstractEndpointSnitch() - { - public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) - { - return 0; - } - - public String getRack(InetAddressAndPort endpoint) - { - return "R1"; - } - - public String getDatacenter(InetAddressAndPort endpoint) - { - return "DC1"; - } - }; - - return new SimpleStrategy("MoveTransientTest", - tmd, - snitch, - com.google.common.collect.ImmutableMap.of("replication_factor", "3/1")); + return AbstractReplicationStrategy.createReplicationStrategy(KEYSPACE, metadata.schema.getKeyspaceMetadata(KEYSPACE).params.replication); } } diff --git a/test/unit/org/apache/cassandra/service/ClientStateTest.java b/test/unit/org/apache/cassandra/service/ClientStateTest.java index 04bddb4169..f037eb2c35 100644 --- a/test/unit/org/apache/cassandra/service/ClientStateTest.java +++ b/test/unit/org/apache/cassandra/service/ClientStateTest.java @@ -21,14 +21,12 @@ package org.apache.cassandra.service; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import com.google.common.collect.Iterables; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.auth.AuthCacheService; -import org.apache.cassandra.auth.AuthKeyspace; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.DataResource; @@ -37,9 +35,6 @@ import org.apache.cassandra.auth.Permission; import org.apache.cassandra.auth.Roles; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.distributed.shared.WithProperties; -import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.SchemaConstants; -import org.apache.cassandra.schema.TableMetadata; import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; import static org.junit.Assert.assertEquals; @@ -55,11 +50,7 @@ public class ClientStateTest properties = new WithProperties().set(ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION, true); SchemaLoader.prepareServer(); DatabaseDescriptor.setAuthFromRoot(true); - // create the system_auth keyspace so the IRoleManager can function as normal - SchemaLoader.createKeyspace(SchemaConstants.AUTH_KEYSPACE_NAME, - KeyspaceParams.simple(1), - Iterables.toArray(AuthKeyspace.metadata().tables, TableMetadata.class)); - + Roles.init(); AuthCacheService.initializeAndRegisterCaches(); } diff --git a/test/unit/org/apache/cassandra/service/DefaultFSErrorHandlerTest.java b/test/unit/org/apache/cassandra/service/DefaultFSErrorHandlerTest.java index f6ed9da7ed..b1da62fcdc 100644 --- a/test/unit/org/apache/cassandra/service/DefaultFSErrorHandlerTest.java +++ b/test/unit/org/apache/cassandra/service/DefaultFSErrorHandlerTest.java @@ -39,6 +39,7 @@ import org.apache.cassandra.io.FSErrorHandler; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.io.sstable.CorruptSSTableException; +import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -54,6 +55,7 @@ public class DefaultFSErrorHandlerTest @BeforeClass public static void defineSchema() throws ConfigurationException { + JOIN_RING.setBoolean(false); // required to start gossiper without setting tokens SchemaLoader.prepareServer(); CassandraDaemon daemon = new CassandraDaemon(); daemon.completeSetup(); //startup must be completed, otherwise FS error will kill JVM regardless of failure policy diff --git a/test/unit/org/apache/cassandra/service/DiskFailurePolicyTest.java b/test/unit/org/apache/cassandra/service/DiskFailurePolicyTest.java index b041a1cd10..832c631eed 100644 --- a/test/unit/org/apache/cassandra/service/DiskFailurePolicyTest.java +++ b/test/unit/org/apache/cassandra/service/DiskFailurePolicyTest.java @@ -42,6 +42,7 @@ import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.KillerForTests; +import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING; import static org.apache.cassandra.config.Config.DiskFailurePolicy.best_effort; import static org.apache.cassandra.config.Config.DiskFailurePolicy.die; import static org.apache.cassandra.config.Config.DiskFailurePolicy.ignore; @@ -68,6 +69,7 @@ public class DiskFailurePolicyTest @BeforeClass public static void defineSchema() throws ConfigurationException { + JOIN_RING.setBoolean(false); // required to start gossiper without setting tokens SchemaLoader.prepareServer(); StorageService.instance.initServer(); FileUtils.setFSErrorHandler(new DefaultFSErrorHandler()); diff --git a/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java b/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java index c2aeb56710..ec2a373ccb 100644 --- a/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java +++ b/test/unit/org/apache/cassandra/service/JoinTokenRingTest.java @@ -18,17 +18,24 @@ package org.apache.cassandra.service; import java.io.IOException; +import java.util.concurrent.ExecutionException; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.index.StubIndex; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.FBUtilities; public class JoinTokenRingTest { @@ -36,19 +43,26 @@ public class JoinTokenRingTest public static void setup() throws ConfigurationException { DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); SchemaLoader.startGossiper(); - SchemaLoader.prepareServer(); SchemaLoader.schemaDefinition("JoinTokenRingTest"); } @Test - public void testIndexPreJoinInvocation() throws IOException + public void testIndexPreJoinInvocation() throws IOException, ExecutionException, InterruptedException { - StorageService ss = StorageService.instance; - ss.joinRing(); - + ClusterMetadataTestHelper.addEndpoint(FBUtilities.getBroadcastAddressAndPort(), + ClusterMetadata.current().partitioner.getRandomToken()); + ScheduledExecutors.optionalTasks.submit(() -> null).get(); // make sure the LegacyStateListener has finished executing SecondaryIndexManager indexManager = ColumnFamilyStore.getIfExists("JoinTokenRingTestKeyspace7", "Indexed1").indexManager; StubIndex stub = (StubIndex) indexManager.getIndexByName("Indexed1_value_index"); Assert.assertTrue(stub.preJoinInvocation); } + + + @Test @Ignore("Implement equivalent test when joining from write survey mode is supported") + public void testIndexPreJoinInvocationFromWriteSurveyMode() throws IOException + { + // TODO implement new test + } } diff --git a/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java b/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java deleted file mode 100644 index 5d4f681e5c..0000000000 --- a/test/unit/org/apache/cassandra/service/LeaveAndBootstrapTest.java +++ /dev/null @@ -1,739 +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; - -import java.net.UnknownHostException; -import java.util.*; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.Util; -import org.apache.cassandra.Util.PartitionerSwitcher; -import org.apache.cassandra.config.CassandraRelevantProperties; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.db.SystemKeyspace; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.RandomPartitioner; -import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.SimpleSnitch; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.schema.KeyspaceMetadata; - -import static org.junit.Assert.*; - -public class LeaveAndBootstrapTest -{ - private static final IPartitioner partitioner = RandomPartitioner.instance; - private static PartitionerSwitcher partitionerSwitcher; - private static final String KEYSPACE1 = "LeaveAndBootstrapTestKeyspace1"; - private static final String KEYSPACE2 = "LeaveAndBootstrapTestKeyspace2"; - private static final String KEYSPACE3 = "LeaveAndBootstrapTestKeyspace3"; - private static final String KEYSPACE4 = "LeaveAndBootstrapTestKeyspace4"; - - @BeforeClass - public static void defineSchema() throws Exception - { - CassandraRelevantProperties.GOSSIPER_QUARANTINE_DELAY.setLong(10000); - DatabaseDescriptor.daemonInitialization(); - partitionerSwitcher = Util.switchPartitioner(partitioner); - SchemaLoader.loadSchema(); - SchemaLoader.schemaDefinition("LeaveAndBootstrapTest"); - } - - @AfterClass - public static void tearDown() - { - partitionerSwitcher.close(); - } - - /** - * Test whether write endpoints is correct when the node is leaving. Uses - * StorageService.onChange and does not manipulate token metadata directly. - */ - @Test - public void newTestWriteEndpointsDuringLeave() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 6; - final int LEAVING_NODE = 3; - - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList<>(); - - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - - Map> expectedEndpoints = new HashMap<>(); - for (Token token : keyTokens) - { - List endpoints = new ArrayList<>(); - Iterator tokenIter = TokenMetadata.ringIterator(tmd.sortedTokens(), token, false); - while (tokenIter.hasNext()) - { - endpoints.add(tmd.getEndpoint(tokenIter.next())); - } - expectedEndpoints.put(token, endpoints); - } - - // Third node leaves - ss.onChange(hosts.get(LEAVING_NODE), - ApplicationState.STATUS_WITH_PORT, - valueFactory.leaving(Collections.singleton(endpointTokens.get(LEAVING_NODE)))); - ss.onChange(hosts.get(LEAVING_NODE), - ApplicationState.STATUS, - valueFactory.leaving(Collections.singleton(endpointTokens.get(LEAVING_NODE)))); - assertTrue(tmd.isLeaving(hosts.get(LEAVING_NODE))); - - Thread.sleep(100); // because there is a tight race between submit and blockUntilFinished - PendingRangeCalculatorService.instance.blockUntilFinished(); - - AbstractReplicationStrategy strategy; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - { - strategy = getStrategy(keyspaceName, tmd); - for (Token token : keyTokens) - { - int replicationFactor = strategy.getReplicationFactor().allReplicas; - - Set actual = tmd.getWriteEndpoints(token, keyspaceName, strategy.calculateNaturalReplicas(token, tmd.cloneOnlyTokenMap()).forToken(token)).endpoints(); - Set expected = new HashSet<>(); - - for (int i = 0; i < replicationFactor; i++) - { - expected.add(expectedEndpoints.get(token).get(i)); - } - - // if the leaving node is in the endpoint list, - // then we should expect it plus one extra for when it's gone - if (expected.contains(hosts.get(LEAVING_NODE))) - expected.add(expectedEndpoints.get(token).get(replicationFactor)); - - assertEquals("mismatched endpoint sets", expected, actual); - } - } - } - - /** - * Test pending ranges and write endpoints when multiple nodes are on the move - * simultaneously - */ - @Test - public void testSimultaneousMove() throws UnknownHostException - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 10; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 10 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - - // nodes 6, 8 and 9 leave - final int[] LEAVING = new int[]{ 6, 8, 9 }; - for (int leaving : LEAVING) - { - ss.onChange(hosts.get(leaving), - ApplicationState.STATUS_WITH_PORT, - valueFactory.leaving(Collections.singleton(endpointTokens.get(leaving)))); - ss.onChange(hosts.get(leaving), - ApplicationState.STATUS, - valueFactory.leaving(Collections.singleton(endpointTokens.get(leaving)))); - } - - // boot two new nodes with keyTokens.get(5) and keyTokens.get(7) - InetAddressAndPort boot1 = InetAddressAndPort.getByName("127.0.1.1"); - Gossiper.instance.initializeNodeUnsafe(boot1, UUID.randomUUID(), 1); - Gossiper.instance.injectApplicationState(boot1, ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(5)))); - ss.onChange(boot1, - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(5)))); - InetAddressAndPort boot2 = InetAddressAndPort.getByName("127.0.1.2"); - Gossiper.instance.initializeNodeUnsafe(boot2, UUID.randomUUID(), 1); - Gossiper.instance.injectApplicationState(boot2, ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(7)))); - ss.onChange(boot2, - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(7)))); - - /* don't require test update every time a new keyspace is added to test/conf/cassandra.yaml */ - Map keyspaceStrategyMap = new HashMap(); - for (int i=1; i<=4; i++) - { - keyspaceStrategyMap.put("LeaveAndBootstrapTestKeyspace" + i, getStrategy("LeaveAndBootstrapTestKeyspace" + i, tmd)); - } - - // pre-calculate the results. - Map> expectedEndpoints = new HashMap>(); - expectedEndpoints.put(KEYSPACE1, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.8")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.1.2", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.10", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE1).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.1")); - expectedEndpoints.put(KEYSPACE2, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.8")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.1.2", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.10", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.1")); - expectedEndpoints.put(KEYSPACE3, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.1.1", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.1.2", "127.0.0.1", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.1.2", "127.0.0.1", "127.0.0.2", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.1.2", "127.0.0.1", "127.0.0.2", "127.0.1.1", "127.0.0.3")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.1.2", "127.0.0.3", "127.0.0.4")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.1.2", "127.0.0.4", "127.0.0.5")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5")); - expectedEndpoints.put(KEYSPACE4, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2", "127.0.0.3", "127.0.0.4")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3", "127.0.0.4", "127.0.0.5")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4", "127.0.0.5", "127.0.0.6")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.1.1", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.1.2", "127.0.0.1", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.1", "127.0.0.2", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.1.2", "127.0.0.1", "127.0.0.2")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.1.2", "127.0.0.2", "127.0.0.3")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3")); - expectedEndpoints.get(KEYSPACE4).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.1", "127.0.0.2", "127.0.0.3")); - - PendingRangeCalculatorService.instance.blockUntilFinished(); - - for (Map.Entry keyspaceStrategy : keyspaceStrategyMap.entrySet()) - { - String keyspaceName = keyspaceStrategy.getKey(); - AbstractReplicationStrategy strategy = keyspaceStrategy.getValue(); - - for (int i = 0; i < keyTokens.size(); i++) - { - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(i), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(i))).endpoints(); - assertEquals(expectedEndpoints.get(keyspaceName).get(keyTokens.get(i)).size(), endpoints.size()); - assertTrue(expectedEndpoints.get(keyspaceName).get(keyTokens.get(i)).containsAll(endpoints)); - } - - // just to be sure that things still work according to the old tests, run them: - if (strategy.getReplicationFactor().allReplicas != 3) - continue; - // tokens 5, 15 and 25 should go three nodes - for (int i=0; i<3; ++i) - { - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(i), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(i))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(i+1))); - assertTrue(endpoints.contains(hosts.get(i+2))); - assertTrue(endpoints.contains(hosts.get(i+3))); - } - - // token 35 should go to nodes 4, 5, 6, 7 and boot1 - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(3), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(3))).endpoints(); - assertEquals(5, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(4))); - assertTrue(endpoints.contains(hosts.get(5))); - assertTrue(endpoints.contains(hosts.get(6))); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(boot1)); - - // token 45 should go to nodes 5, 6, 7, 0, boot1 and boot2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(4), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(4))).endpoints(); - assertEquals(6, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(5))); - assertTrue(endpoints.contains(hosts.get(6))); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(boot1)); - assertTrue(endpoints.contains(boot2)); - - // token 55 should go to nodes 6, 7, 8, 0, 1, boot1 and boot2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(5), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(5))).endpoints(); - assertEquals(7, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(6))); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(boot1)); - assertTrue(endpoints.contains(boot2)); - - // token 65 should go to nodes 7, 8, 9, 0, 1 and boot2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(6), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(6))).endpoints(); - assertEquals(6, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(9))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(boot2)); - - // token 75 should to go nodes 8, 9, 0, 1, 2 and boot2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(7), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(7))).endpoints(); - assertEquals(6, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(9))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - assertTrue(endpoints.contains(boot2)); - - // token 85 should go to nodes 9, 0, 1 and 2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(8), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(8))).endpoints(); - assertEquals(4, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(9))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - - // token 95 should go to nodes 0, 1 and 2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(9), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(9))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - - } - - // Now finish node 6 and node 9 leaving, as well as boot1 (after this node 8 is still - // leaving and boot2 in progress - ss.onChange(hosts.get(LEAVING[0]), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(endpointTokens.get(LEAVING[0])), Gossiper.computeExpireTime())); - ss.onChange(hosts.get(LEAVING[2]), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(endpointTokens.get(LEAVING[2])), Gossiper.computeExpireTime())); - ss.onChange(boot1, ApplicationState.STATUS, valueFactory.normal(Collections.singleton(keyTokens.get(5)))); - - // adjust precalcuated results. this changes what the epected endpoints are. - expectedEndpoints.get(KEYSPACE1).get(new BigIntegerToken("55")).removeAll(makeAddrs("127.0.0.7", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE1).get(new BigIntegerToken("85")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE2).get(new BigIntegerToken("55")).removeAll(makeAddrs("127.0.0.7", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE2).get(new BigIntegerToken("85")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("15")).removeAll(makeAddrs("127.0.0.7", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("25")).removeAll(makeAddrs("127.0.0.7", "127.0.1.2", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("35")).removeAll(makeAddrs("127.0.0.7", "127.0.0.2")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("45")).removeAll(makeAddrs("127.0.0.7", "127.0.0.10", "127.0.0.3")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("55")).removeAll(makeAddrs("127.0.0.7", "127.0.0.10", "127.0.0.4")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("65")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("75")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE3).get(new BigIntegerToken("85")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("35")).removeAll(makeAddrs("127.0.0.7", "127.0.0.8")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("45")).removeAll(makeAddrs("127.0.0.7", "127.0.1.2", "127.0.0.1")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("55")).removeAll(makeAddrs("127.0.0.2", "127.0.0.7")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("65")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("75")).removeAll(makeAddrs("127.0.0.10")); - expectedEndpoints.get(KEYSPACE4).get(new BigIntegerToken("85")).removeAll(makeAddrs("127.0.0.10")); - - PendingRangeCalculatorService.instance.blockUntilFinished(); - - for (Map.Entry keyspaceStrategy : keyspaceStrategyMap.entrySet()) - { - String keyspaceName = keyspaceStrategy.getKey(); - AbstractReplicationStrategy strategy = keyspaceStrategy.getValue(); - - for (int i = 0; i < keyTokens.size(); i++) - { - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(i), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(i))).endpoints(); - assertEquals(expectedEndpoints.get(keyspaceName).get(keyTokens.get(i)).size(), endpoints.size()); - assertTrue(expectedEndpoints.get(keyspaceName).get(keyTokens.get(i)).containsAll(endpoints)); - } - - if (strategy.getReplicationFactor().allReplicas != 3) - continue; - // leave this stuff in to guarantee the old tests work the way they were supposed to. - // tokens 5, 15 and 25 should go three nodes - for (int i=0; i<3; ++i) - { - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(i), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(i))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(i+1))); - assertTrue(endpoints.contains(hosts.get(i+2))); - assertTrue(endpoints.contains(hosts.get(i+3))); - } - - // token 35 goes to nodes 4, 5 and boot1 - Collection endpoints = tmd.getWriteEndpoints(keyTokens.get(3), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(3))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(4))); - assertTrue(endpoints.contains(hosts.get(5))); - assertTrue(endpoints.contains(boot1)); - - // token 45 goes to nodes 5, boot1 and node7 - endpoints = tmd.getWriteEndpoints(keyTokens.get(4), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(4))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(5))); - assertTrue(endpoints.contains(boot1)); - assertTrue(endpoints.contains(hosts.get(7))); - - // token 55 goes to boot1, 7, boot2, 8 and 0 - endpoints = tmd.getWriteEndpoints(keyTokens.get(5), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(5))).endpoints(); - assertEquals(5, endpoints.size()); - assertTrue(endpoints.contains(boot1)); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(boot2)); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(0))); - - // token 65 goes to nodes 7, boot2, 8, 0 and 1 - endpoints = tmd.getWriteEndpoints(keyTokens.get(6), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(6))).endpoints(); - assertEquals(5, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(7))); - assertTrue(endpoints.contains(boot2)); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - - // token 75 goes to nodes boot2, 8, 0, 1 and 2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(7), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(7))).endpoints(); - assertEquals(5, endpoints.size()); - assertTrue(endpoints.contains(boot2)); - assertTrue(endpoints.contains(hosts.get(8))); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - - // token 85 goes to nodes 0, 1 and 2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(8), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(8))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - - // token 95 goes to nodes 0, 1 and 2 - endpoints = tmd.getWriteEndpoints(keyTokens.get(9), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(9))).endpoints(); - assertEquals(3, endpoints.size()); - assertTrue(endpoints.contains(hosts.get(0))); - assertTrue(endpoints.contains(hosts.get(1))); - assertTrue(endpoints.contains(hosts.get(2))); - } - } - - @Test - public void testStateJumpToBootstrap() throws UnknownHostException - { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 5 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 7); - - // node 2 leaves - ss.onChange(hosts.get(2), - ApplicationState.STATUS, - valueFactory.leaving(Collections.singleton(endpointTokens.get(2)))); - - // don't bother to test pending ranges here, that is extensively tested by other - // tests. Just check that the node is in appropriate lists. - assertTrue(tmd.isMember(hosts.get(2))); - assertTrue(tmd.isLeaving(hosts.get(2))); - assertTrue(tmd.getBootstrapTokens().isEmpty()); - - // Bootstrap the node immedidiately to keyTokens.get(4) without going through STATE_LEFT - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(4)))); - ss.onChange(hosts.get(2), - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(4)))); - - assertFalse(tmd.isMember(hosts.get(2))); - assertFalse(tmd.isLeaving(hosts.get(2))); - assertEquals(hosts.get(2), tmd.getBootstrapTokens().get(keyTokens.get(4))); - - // Bootstrap node hosts.get(3) to keyTokens.get(1) - Gossiper.instance.injectApplicationState(hosts.get(3), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(1)))); - ss.onChange(hosts.get(3), - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(1)))); - - assertFalse(tmd.isMember(hosts.get(3))); - assertFalse(tmd.isLeaving(hosts.get(3))); - assertEquals(hosts.get(2), tmd.getBootstrapTokens().get(keyTokens.get(4))); - assertEquals(hosts.get(3), tmd.getBootstrapTokens().get(keyTokens.get(1))); - - // Bootstrap node hosts.get(2) further to keyTokens.get(3) - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(3)))); - ss.onChange(hosts.get(2), - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(3)))); - - assertFalse(tmd.isMember(hosts.get(2))); - assertFalse(tmd.isLeaving(hosts.get(2))); - assertEquals(hosts.get(2), tmd.getBootstrapTokens().get(keyTokens.get(3))); - assertNull(tmd.getBootstrapTokens().get(keyTokens.get(4))); - assertEquals(hosts.get(3), tmd.getBootstrapTokens().get(keyTokens.get(1))); - - // Go to normal again for both nodes - Gossiper.instance.injectApplicationState(hosts.get(3), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(2)))); - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(3)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(keyTokens.get(3)))); - ss.onChange(hosts.get(3), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(keyTokens.get(2)))); - - assertTrue(tmd.isMember(hosts.get(2))); - assertFalse(tmd.isLeaving(hosts.get(2))); - assertEquals(keyTokens.get(3), tmd.getTokens(hosts.get(2)).iterator().next()); - assertTrue(tmd.isMember(hosts.get(3))); - assertFalse(tmd.isLeaving(hosts.get(3))); - assertEquals(keyTokens.get(2), tmd.getTokens(hosts.get(3)).iterator().next()); - - assertTrue(tmd.getBootstrapTokens().isEmpty()); - } - - @Test - public void testStateJumpToNormal() throws UnknownHostException - { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 5 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 6); - - // node 2 leaves - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.leaving(Collections.singleton(endpointTokens.get(2)))); - - assertTrue(tmd.isLeaving(hosts.get(2))); - assertEquals(endpointTokens.get(2), tmd.getTokens(hosts.get(2)).iterator().next()); - - // back to normal - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(2)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(keyTokens.get(2)))); - - assertTrue(tmd.getSizeOfLeavingEndpoints() == 0); - assertEquals(keyTokens.get(2), tmd.getTokens(hosts.get(2)).iterator().next()); - - // node 3 goes through leave and left and then jumps to normal at its new token - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.leaving(Collections.singleton(keyTokens.get(2)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(keyTokens.get(2)), Gossiper.computeExpireTime())); - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(4)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(keyTokens.get(4)))); - - assertTrue(tmd.getBootstrapTokens().isEmpty()); - assertTrue(tmd.getSizeOfLeavingEndpoints() == 0); - assertEquals(keyTokens.get(4), tmd.getTokens(hosts.get(2)).iterator().next()); - } - - @Test - public void testStateJumpToLeaving() throws UnknownHostException - { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 5 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 6); - - // node 2 leaves with _different_ token - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(0)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.leaving(Collections.singleton(keyTokens.get(0)))); - - assertEquals(keyTokens.get(0), tmd.getTokens(hosts.get(2)).iterator().next()); - assertTrue(tmd.isLeaving(hosts.get(2))); - assertNull(tmd.getEndpoint(endpointTokens.get(2))); - - // go to boostrap - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(1)))); - ss.onChange(hosts.get(2), - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(1)))); - - assertFalse(tmd.isLeaving(hosts.get(2))); - assertEquals(1, tmd.getBootstrapTokens().size()); - assertEquals(hosts.get(2), tmd.getBootstrapTokens().get(keyTokens.get(1))); - - // jump to leaving again - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.leaving(Collections.singleton(keyTokens.get(1)))); - - assertEquals(hosts.get(2), tmd.getEndpoint(keyTokens.get(1))); - assertTrue(tmd.isLeaving(hosts.get(2))); - assertTrue(tmd.getBootstrapTokens().isEmpty()); - - // go to state left - ss.onChange(hosts.get(2), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(keyTokens.get(1)), Gossiper.computeExpireTime())); - - assertFalse(tmd.isMember(hosts.get(2))); - assertFalse(tmd.isLeaving(hosts.get(2))); - } - - @Test - public void testStateJumpToLeft() throws UnknownHostException - { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - Gossiper.instance.clearUnsafe(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring of 6 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 7); - - // node hosts.get(2) goes jumps to left - ss.onChange(hosts.get(2), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(endpointTokens.get(2)), Gossiper.computeExpireTime())); - - assertFalse(tmd.isMember(hosts.get(2))); - - // node hosts.get(4) goes to bootstrap - Gossiper.instance.injectApplicationState(hosts.get(3), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(1)))); - ss.onChange(hosts.get(3), ApplicationState.STATUS, valueFactory.bootstrapping(Collections.singleton(keyTokens.get(1)))); - - assertFalse(tmd.isMember(hosts.get(3))); - assertEquals(1, tmd.getBootstrapTokens().size()); - assertEquals(hosts.get(3), tmd.getBootstrapTokens().get(keyTokens.get(1))); - - // and then directly to 'left' - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(1)))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, - valueFactory.left(Collections.singleton(keyTokens.get(1)), Gossiper.computeExpireTime())); - - assertTrue(tmd.getBootstrapTokens().size() == 0); - assertFalse(tmd.isMember(hosts.get(2))); - assertFalse(tmd.isLeaving(hosts.get(2))); - } - - /** - * Tests that the system.peers table is not updated after a node has been removed. (See CASSANDRA-6053) - */ - @Test - public void testStateChangeOnRemovedNode() throws UnknownHostException - { - StorageService ss = StorageService.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - // create a ring of 2 nodes - ArrayList endpointTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - Util.createInitialRing(ss, partitioner, endpointTokens, new ArrayList(), hosts, new ArrayList(), 2); - - InetAddressAndPort toRemove = hosts.get(1); - SystemKeyspace.updatePeerInfo(toRemove, "data_center", "dc42"); - SystemKeyspace.updatePeerInfo(toRemove, "rack", "rack42"); - assertEquals("rack42", SystemKeyspace.loadDcRackInfo().get(toRemove).get("rack")); - - // mark the node as removed - Gossiper.instance.injectApplicationState(toRemove, ApplicationState.STATUS, - valueFactory.left(Collections.singleton(endpointTokens.get(1)), Gossiper.computeExpireTime())); - assertTrue(Gossiper.instance.isDeadState(Gossiper.instance.getEndpointStateForEndpoint(hosts.get(1)))); - - // state changes made after the endpoint has left should be ignored - ss.onChange(hosts.get(1), ApplicationState.RACK, - valueFactory.rack("rack9999")); - assertEquals("rack42", SystemKeyspace.loadDcRackInfo().get(toRemove).get("rack")); - } - - @Test - public void testRemovingStatusForNonMember() throws UnknownHostException - { - // create a ring of 1 node - StorageService ss = StorageService.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - Util.createInitialRing(ss, partitioner, new ArrayList(), new ArrayList(), new ArrayList(), new ArrayList(), 1); - - // make a REMOVING state change on a non-member endpoint; without the CASSANDRA-6564 fix, this - // would result in an ArrayIndexOutOfBoundsException - ss.onChange(InetAddressAndPort.getByName("192.168.1.42"), ApplicationState.STATUS_WITH_PORT, valueFactory.removingNonlocal(UUID.randomUUID())); - ss.onChange(InetAddressAndPort.getByName("192.168.1.42"), ApplicationState.STATUS, valueFactory.removingNonlocal(UUID.randomUUID())); - } - - private static Collection makeAddrs(String... hosts) throws UnknownHostException - { - ArrayList addrs = new ArrayList<>(hosts.length); - for (String host : hosts) - addrs.add(InetAddressAndPort.getByName(host)); - return addrs; - } - - private AbstractReplicationStrategy getStrategy(String keyspaceName, TokenMetadata tmd) - { - KeyspaceMetadata ksmd = Schema.instance.getKeyspaceMetadata(keyspaceName); - return AbstractReplicationStrategy.createReplicationStrategy( - keyspaceName, - ksmd.params.replication.klass, - tmd, - new SimpleSnitch(), - ksmd.params.replication.options); - } - -} diff --git a/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java b/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java index 1d79eccb43..b54da98f08 100644 --- a/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java +++ b/test/unit/org/apache/cassandra/service/LegacyAuthFailTest.java @@ -37,8 +37,6 @@ public class LegacyAuthFailTest extends CQLTester @Test public void testStartupChecks() throws Throwable { - createKeyspace(); - List legacyTables = new ArrayList<>(SchemaConstants.LEGACY_AUTH_TABLES); // test reporting for individual tables @@ -81,9 +79,4 @@ public class LegacyAuthFailTest extends CQLTester { execute(format("CREATE TABLE %s.%s (id int PRIMARY KEY, val text)", SchemaConstants.AUTH_KEYSPACE_NAME, legacyTable)); } - - private void createKeyspace() throws Throwable - { - execute(format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", SchemaConstants.AUTH_KEYSPACE_NAME)); - } } diff --git a/test/unit/org/apache/cassandra/service/MoveTest.java b/test/unit/org/apache/cassandra/service/MoveTest.java deleted file mode 100644 index a642ff3377..0000000000 --- a/test/unit/org/apache/cassandra/service/MoveTest.java +++ /dev/null @@ -1,1102 +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; - -import java.net.UnknownHostException; -import java.util.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimap; - -import org.apache.cassandra.diag.DiagnosticEventService; -import org.apache.cassandra.gms.GossiperEvent; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.RangesAtEndpoint; -import org.apache.cassandra.locator.RangesByEndpoint; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaCollection; -import org.apache.cassandra.schema.SchemaTestUtil; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.marshal.BytesType; -import org.apache.cassandra.SchemaLoader; -import org.apache.cassandra.Util; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.RandomPartitioner; -import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.locator.AbstractNetworkTopologySnitch; -import org.apache.cassandra.locator.NetworkTopologyStrategy; -import org.apache.cassandra.locator.PendingRangeMaps; -import org.apache.cassandra.gms.VersionedValue; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.SimpleSnitch; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.Tables; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertNotNull; - -public class MoveTest -{ - private static final IPartitioner partitioner = RandomPartitioner.instance; - private static IPartitioner oldPartitioner; - //Simple Strategy Keyspaces - private static final String Simple_RF1_KeyspaceName = "MoveTestKeyspace1"; - private static final String Simple_RF2_KeyspaceName = "MoveTestKeyspace5"; - private static final String Simple_RF3_KeyspaceName = "MoveTestKeyspace4"; - private static final String KEYSPACE2 = "MoveTestKeyspace2"; - private static final String KEYSPACE3 = "MoveTestKeyspace3"; - - //Network Strategy Keyspace with RF DC1=1 and DC2=1 and so on. - private static final String Network_11_KeyspaceName = "MoveTestNetwork11"; - private static final String Network_22_KeyspaceName = "MoveTestNetwork22"; - private static final String Network_33_KeyspaceName = "MoveTestNetwork33"; - - /* - * NOTE: the tests above uses RandomPartitioner, which is not the default - * test partitioner. Changing the partitioner should be done before - * loading the schema as loading the schema trigger the write of sstables. - * So instead of extending SchemaLoader, we call it's method below. - */ - @BeforeClass - public static void setup() throws Exception - { - DatabaseDescriptor.daemonInitialization(); - oldPartitioner = StorageService.instance.setPartitionerUnsafe(partitioner); - SchemaLoader.loadSchema(); - SchemaLoader.schemaDefinition("MoveTest"); - addNetworkTopologyKeyspace(Network_11_KeyspaceName, 1, 1); - addNetworkTopologyKeyspace(Network_22_KeyspaceName, 2, 2); - addNetworkTopologyKeyspace(Network_33_KeyspaceName, 3, 3); - DatabaseDescriptor.setDiagnosticEventsEnabled(true); - } - - @AfterClass - public static void tearDown() - { - StorageService.instance.setPartitionerUnsafe(oldPartitioner); - } - - @Before - public void clearTokenMetadata() throws InterruptedException - { - // we expect to have a single endpoint before running each test method, - // so we have to wait for the GossipStage thread to evict stale endpoints - // from membership before moving on, otherwise it may break other tests as - // things change in the background - final int endpointCount = Gossiper.instance.getEndpointCount() - 1; - final CountDownLatch latch = new CountDownLatch(endpointCount); - Consumer onEndpointEvicted = event -> latch.countDown(); - DiagnosticEventService.instance().subscribe(GossiperEvent.class, - GossiperEvent.GossiperEventType.EVICTED_FROM_MEMBERSHIP, - onEndpointEvicted); - - PendingRangeCalculatorService.instance.blockUntilFinished(); - StorageService.instance.getTokenMetadata().clearUnsafe(); - - try - { - if (!latch.await(1, TimeUnit.MINUTES)) - throw new RuntimeException("Took too long to evict stale endpoints."); - } - finally - { - DiagnosticEventService.instance().unsubscribe(onEndpointEvicted); - } - } - - private static void addNetworkTopologyKeyspace(String keyspaceName, Integer... replicas) throws Exception - { - - DatabaseDescriptor.setEndpointSnitch(new AbstractNetworkTopologySnitch() - { - //Odd IPs are in DC1 and Even are in DC2. Endpoints upto .14 will have unique racks and - // then will be same for a set of three. - @Override - public String getRack(InetAddressAndPort endpoint) - { - int ipLastPart = getIPLastPart(endpoint); - if (ipLastPart <= 14) - return UUID.randomUUID().toString(); - else - return "RAC" + (ipLastPart % 3); - } - - @Override - public String getDatacenter(InetAddressAndPort endpoint) - { - if (getIPLastPart(endpoint) % 2 == 0) - return "DC2"; - else - return "DC1"; - } - - private int getIPLastPart(InetAddressAndPort endpoint) - { - String str = endpoint.toString(); - int index = str.lastIndexOf("."); - return Integer.parseInt(str.substring(index + 1).trim().split(":")[0]); - } - }); - - final TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - - tmd.clearUnsafe(); - tmd.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.1")); - tmd.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.0.0.2")); - - KeyspaceMetadata keyspace = KeyspaceMetadata.create(keyspaceName, - KeyspaceParams.nts(configOptions(replicas)), - Tables.of(TableMetadata.builder(keyspaceName, "CF1") - .addPartitionKeyColumn("key", BytesType.instance) - .build())); - - SchemaTestUtil.announceNewKeyspace(keyspace); - } - - private static Object[] configOptions(Integer[] replicas) - { - Object[] configOptions = new Object[(replicas.length * 2)]; - int i = 1, j=0; - for(Integer replica : replicas) - { - if(replica == null) - continue; - configOptions[j++] = "DC" + i++; - configOptions[j++] = replica; - } - return configOptions; - } - - @Test - public void testMoveWithPendingRangesNetworkStrategyRackAwareThirtyNodes() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 60; - - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList<>(); - - for(int i=0; i < RING_SIZE/2; i++) - { - endpointTokens.add(new BigIntegerToken(String.valueOf(10 * i))); - endpointTokens.add(new BigIntegerToken(String.valueOf((10 * i) + 1))); - } - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - //Moving Endpoint 127.0.0.37 in RAC1 with current token 180 - int MOVING_NODE = 36; - moveHost(hosts.get(MOVING_NODE), 215, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(150, 151, "127.0.0.43"), - generatePendingMapEntry(151, 160, "127.0.0.43"),generatePendingMapEntry(160, 161, "127.0.0.43"), - generatePendingMapEntry(161, 170, "127.0.0.43"), generatePendingMapEntry(170, 171, "127.0.0.43"), - generatePendingMapEntry(171, 180, "127.0.0.43"), generatePendingMapEntry(210, 211, "127.0.0.37"), - generatePendingMapEntry(211, 215, "127.0.0.37")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 215, tmd); - - //Moving it back to original spot - moveHost(hosts.get(MOVING_NODE), 180, tmd, valueFactory); - finishMove(hosts.get(MOVING_NODE), 180, tmd); - - } - - @Test - public void testMoveWithPendingRangesNetworkStrategyTenNode() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 14; - - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList<>(); - - for(int i=0; i < RING_SIZE/2; i++) - { - endpointTokens.add(new BigIntegerToken(String.valueOf(10 * i))); - endpointTokens.add(new BigIntegerToken(String.valueOf((10 * i) + 1))); - } - - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - int MOVING_NODE = 0; - moveHost(hosts.get(MOVING_NODE), 5, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.1"), - generatePendingMapEntry(1, 5, "127.0.0.1")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.1"), - generatePendingMapEntry(1, 5, "127.0.0.1")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.1"), - generatePendingMapEntry(1, 5, "127.0.0.1")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 5, tmd); - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.3"), - generatePendingMapEntry(1, 5, "127.0.0.3")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.5"), - generatePendingMapEntry(1, 5, "127.0.0.5")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 1, "127.0.0.7"), - generatePendingMapEntry(1, 5, "127.0.0.7")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - MOVING_NODE = 1; - moveHost(hosts.get(MOVING_NODE), 5, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.2")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.2")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.2")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 5, tmd); - - moveHost(hosts.get(MOVING_NODE), 1, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.4")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.6")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 5, "127.0.0.8")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 1, tmd); - - MOVING_NODE = 3; - moveHost(hosts.get(MOVING_NODE), 25, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 10, "127.0.0.6"), - generatePendingMapEntry(10, 11, "127.0.0.6"), generatePendingMapEntry(21, 25, "127.0.0.4")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(61, 0, "127.0.0.6"), - generatePendingMapEntry(0, 1, "127.0.0.6"), generatePendingMapEntry(21, 25, "127.0.0.4"), - generatePendingMapEntry(11, 20, "127.0.0.4"),generatePendingMapEntry(20, 21, "127.0.0.4")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(51, 60, "127.0.0.6"), - generatePendingMapEntry(60, 61, "127.0.0.6"), generatePendingMapEntry(21, 25, "127.0.0.4"), - generatePendingMapEntry(11, 20, "127.0.0.4"), generatePendingMapEntry(20, 21, "127.0.0.4")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 25, tmd); - - moveHost(hosts.get(MOVING_NODE), 11, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1, 10, "127.0.0.4"), - generatePendingMapEntry(10, 11, "127.0.0.4"), generatePendingMapEntry(21, 25, "127.0.0.8")), Network_11_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(61, 0, "127.0.0.4"), - generatePendingMapEntry(0, 1, "127.0.0.4"), generatePendingMapEntry(11, 20, "127.0.0.8"), - generatePendingMapEntry(20, 21, "127.0.0.8"), generatePendingMapEntry(21, 25, "127.0.0.10")), Network_22_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(51, 60, "127.0.0.4"), - generatePendingMapEntry(60, 61, "127.0.0.4"), generatePendingMapEntry(21, 25, "127.0.0.12"), - generatePendingMapEntry(11, 20, "127.0.0.10"), generatePendingMapEntry(20, 21, "127.0.0.10")), Network_33_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 11, tmd); - } - - @Test - public void testMoveWithPendingRangesSimpleStrategyTenNode() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 10; - - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList<>(); - - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - final int MOVING_NODE = 0; // index of the moving node - moveHost(hosts.get(MOVING_NODE), 2, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 2, tmd); - - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - moveHost(hosts.get(MOVING_NODE), 1000, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 1000, tmd); - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1000, 0, "127.0.0.1")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - moveHost(hosts.get(MOVING_NODE), 35, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 35, "127.0.0.1"), generatePendingMapEntry(90, 0, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 35, "127.0.0.1"), generatePendingMapEntry(20, 30, "127.0.0.1"), - generatePendingMapEntry(80, 90, "127.0.0.2"), generatePendingMapEntry(90, 0, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 35, "127.0.0.1"), generatePendingMapEntry(20, 30, "127.0.0.1"), - generatePendingMapEntry(80, 90, "127.0.0.3"), generatePendingMapEntry(90, 0, "127.0.0.4"), - generatePendingMapEntry(10, 20, "127.0.0.1"), generatePendingMapEntry(70, 80, "127.0.0.2")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 35, tmd); - - } - - @Test - public void testMoveWithPendingRangesForSimpleStrategyFourNode() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 4; - - TokenMetadata tmd = ss.getTokenMetadata(); - tmd.clearUnsafe(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - ArrayList endpointTokens = new ArrayList<>(); - ArrayList keyTokens = new ArrayList<>(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList<>(); - - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - int MOVING_NODE = 0; // index of the moving node - moveHost(hosts.get(MOVING_NODE), 2, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.1")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 2, tmd); - - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 2, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - moveHost(hosts.get(MOVING_NODE), 1500, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 1500, tmd); - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(1500, 0, "127.0.0.1")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - moveHost(hosts.get(MOVING_NODE), 15, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(10, 15, "127.0.0.1"), generatePendingMapEntry(30, 0, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(20, 30, "127.0.0.2"), generatePendingMapEntry(10, 15, "127.0.0.1"), - generatePendingMapEntry(0, 10, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(15, 20, "127.0.0.2"), - generatePendingMapEntry(0, 10, "127.0.0.1")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 15, tmd); - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 0, "127.0.0.1"), - generatePendingMapEntry(10, 15, "127.0.0.3")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(20, 30, "127.0.0.1"), - generatePendingMapEntry(10, 15, "127.0.0.4"), generatePendingMapEntry(0, 10, "127.0.0.3")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(15, 20, "127.0.0.1"), - generatePendingMapEntry(0, 10, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - moveHost(hosts.get(MOVING_NODE), 26, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(20, 26, "127.0.0.1"), - generatePendingMapEntry(30, 0, "127.0.0.2")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(26, 30, "127.0.0.2"), - generatePendingMapEntry(30, 0, "127.0.0.3"), generatePendingMapEntry(10, 20, "127.0.0.1")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(0, 10, "127.0.0.1"), - generatePendingMapEntry(26, 30, "127.0.0.3")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 26, tmd); - - moveHost(hosts.get(MOVING_NODE), 0, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(20, 26, "127.0.0.4"), - generatePendingMapEntry(30, 0, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 0, "127.0.0.1"), - generatePendingMapEntry(26, 30, "127.0.0.1"), generatePendingMapEntry(10, 20, "127.0.0.4")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(26, 30, "127.0.0.1"), - generatePendingMapEntry(0, 10, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 0, tmd); - - MOVING_NODE = 3; - - moveHost(hosts.get(MOVING_NODE), 33, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.4")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.4")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.4")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 33, tmd); - - moveHost(hosts.get(MOVING_NODE), 30, tmd, valueFactory); - - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.1")), Simple_RF1_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.2")), Simple_RF2_KeyspaceName); - assertPendingRanges(tmd, generatePendingRanges(generatePendingMapEntry(30, 33, "127.0.0.3")), Simple_RF3_KeyspaceName); - - finishMove(hosts.get(MOVING_NODE), 30, tmd); - } - - private void moveHost(InetAddressAndPort host, int token, TokenMetadata tmd, VersionedValue.VersionedValueFactory valueFactory ) - { - StorageService.instance.onChange(host, ApplicationState.STATUS, valueFactory.moving(new BigIntegerToken(String.valueOf(token)))); - PendingRangeCalculatorService.instance.blockUntilFinished(); - assertTrue(tmd.isMoving(host)); - } - - private void finishMove(InetAddressAndPort host, int token, TokenMetadata tmd) - { - tmd.removeFromMoving(host); - assertTrue(!tmd.isMoving(host)); - Token newToken = new BigIntegerToken(String.valueOf(token)); - tmd.updateNormalToken(newToken, host); - // As well as upating TMD, update the host's tokens in gossip. Since CASSANDRA-15120, status changing to MOVING - // ensures that TMD is up to date with token assignments according to gossip. So we need to make sure gossip has - // the correct new token, as the moving node itself would do upon successful completion of the move operation. - // Without this, the next movement for that host will set the token in TMD's back to the old value from gossip - // and incorrect range movements will follow - Gossiper.instance.injectApplicationState(host, - ApplicationState.TOKENS, - new VersionedValue.VersionedValueFactory(partitioner).tokens(Collections.singleton(newToken))); - } - - private Map.Entry, EndpointsForRange> generatePendingMapEntry(int start, int end, String... endpoints) throws UnknownHostException - { - Map, EndpointsForRange> pendingRanges = new HashMap<>(); - Range range = generateRange(start, end); - pendingRanges.put(range, makeReplicas(range, endpoints)); - return pendingRanges.entrySet().iterator().next(); - } - - private Map, EndpointsForRange> generatePendingRanges(Map.Entry, EndpointsForRange>... entries) - { - Map, EndpointsForRange> pendingRanges = new HashMap<>(); - for(Map.Entry, EndpointsForRange> entry : entries) - { - pendingRanges.put(entry.getKey(), entry.getValue()); - } - return pendingRanges; - } - - private void assertPendingRanges(TokenMetadata tmd, Map, EndpointsForRange> pendingRanges, String keyspaceName) throws ConfigurationException - { - boolean keyspaceFound = false; - for (String nonSystemKeyspaceName : Schema.instance.distributedKeyspaces().names()) - { - if(!keyspaceName.equals(nonSystemKeyspaceName)) - continue; - assertMaps(pendingRanges, tmd.getPendingRanges(keyspaceName)); - keyspaceFound = true; - } - - assert keyspaceFound; - } - - private void assertMaps(Map, EndpointsForRange> expected, PendingRangeMaps actual) - { - int sizeOfActual = 0; - Iterator, EndpointsForRange.Builder>> iterator = actual.iterator(); - while(iterator.hasNext()) - { - Map.Entry, EndpointsForRange.Builder> actualEntry = iterator.next(); - assertNotNull(expected.get(actualEntry.getKey())); - assertEquals(ImmutableSet.copyOf(expected.get(actualEntry.getKey())), ImmutableSet.copyOf(actualEntry.getValue())); - sizeOfActual++; - } - - assertEquals(expected.size(), sizeOfActual); - } - - /* - * Test whether write endpoints is correct when the node is moving. Uses - * StorageService.onChange and does not manipulate token metadata directly. - */ - @Test - public void newTestWriteEndpointsDuringMove() throws Exception - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 10; - final int MOVING_NODE = 3; // index of the moving node - - TokenMetadata tmd = ss.getTokenMetadata(); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - - Map> expectedEndpoints = new HashMap<>(); - for (Token token : keyTokens) - { - List endpoints = new ArrayList<>(); - Iterator tokenIter = TokenMetadata.ringIterator(tmd.sortedTokens(), token, false); - while (tokenIter.hasNext()) - { - endpoints.add(tmd.getEndpoint(tokenIter.next())); - } - expectedEndpoints.put(token, endpoints); - } - - // node LEAVING_NODE should move to this token - Token newToken = positionToken(MOVING_NODE); - - // Third node leaves - ss.onChange(hosts.get(MOVING_NODE), ApplicationState.STATUS, valueFactory.moving(newToken)); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - assertTrue(tmd.isMoving(hosts.get(MOVING_NODE))); - - AbstractReplicationStrategy strategy; - for (String keyspaceName : Schema.instance.distributedKeyspaces().names()) - { - strategy = getStrategy(keyspaceName, tmd); - if(strategy instanceof NetworkTopologyStrategy) - continue; - int numMoved = 0; - for (Token token : keyTokens) - { - int replicationFactor = strategy.getReplicationFactor().allReplicas; - - EndpointsForToken actual = tmd.getWriteEndpoints(token, keyspaceName, strategy.calculateNaturalReplicas(token, tmd.cloneOnlyTokenMap()).forToken(token)); - HashSet expected = new HashSet<>(); - - for (int i = 0; i < replicationFactor; i++) - { - expected.add(expectedEndpoints.get(token).get(i)); - } - - if (expected.size() == actual.size()) { - assertEquals("mismatched endpoint sets", expected, actual.endpoints()); - } else { - expected.add(hosts.get(MOVING_NODE)); - assertEquals("mismatched endpoint sets", expected, actual.endpoints()); - numMoved++; - } - } - assertEquals("mismatched number of moved token", 1, numMoved); - } - - // moving endpoint back to the normal state - ss.onChange(hosts.get(MOVING_NODE), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(newToken))); - } - - /* - * Test ranges and write endpoints when multiple nodes are on the move simultaneously - */ - @Test - public void testSimultaneousMove() throws UnknownHostException - { - StorageService ss = StorageService.instance; - final int RING_SIZE = 10; - TokenMetadata tmd = ss.getTokenMetadata(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 10 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); - - // nodes 6, 8 and 9 leave - final int[] MOVING = new int[] {6, 8, 9}; - - Map newTokens = new HashMap(); - - for (int movingIndex : MOVING) - { - Token newToken = positionToken(movingIndex); - ss.onChange(hosts.get(movingIndex), ApplicationState.STATUS, valueFactory.moving(newToken)); - - // storing token associated with a node index - newTokens.put(movingIndex, newToken); - } - - tmd = tmd.cloneAfterAllSettled(); - ss.setTokenMetadataUnsafe(tmd); - - // boot two new nodes with keyTokens.get(5) and keyTokens.get(7) - InetAddressAndPort boot1 = InetAddressAndPort.getByName("127.0.1.1"); - Gossiper.instance.initializeNodeUnsafe(boot1, UUID.randomUUID(), 1); - Gossiper.instance.injectApplicationState(boot1, ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(5)))); - ss.onChange(boot1, - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(5)))); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - InetAddressAndPort boot2 = InetAddressAndPort.getByName("127.0.1.2"); - Gossiper.instance.initializeNodeUnsafe(boot2, UUID.randomUUID(), 1); - Gossiper.instance.injectApplicationState(boot2, ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(keyTokens.get(7)))); - ss.onChange(boot2, - ApplicationState.STATUS, - valueFactory.bootstrapping(Collections.singleton(keyTokens.get(7)))); - PendingRangeCalculatorService.instance.blockUntilFinished(); - - // don't require test update every time a new keyspace is added to test/conf/cassandra.yaml - Map keyspaceStrategyMap = new HashMap(); - for (int i = 1; i <= 4; i++) - { - keyspaceStrategyMap.put("MoveTestKeyspace" + i, getStrategy("MoveTestKeyspace" + i, tmd)); - } - - /** - * Keyspace1 & Keyspace2 RF=1 - * { - * /127.0.0.1=[(97,0]], - * /127.0.0.2=[(0,10]], - * /127.0.0.3=[(10,20]], - * /127.0.0.4=[(20,30]], - * /127.0.0.5=[(30,40]], - * /127.0.0.6=[(40,50]], - * /127.0.0.7=[(50,67]], - * /127.0.0.8=[(67,70]], - * /127.0.0.9=[(70,87]], - * /127.0.0.10=[(87,97]] - * } - */ - - RangesByEndpoint keyspace1ranges = keyspaceStrategyMap.get(Simple_RF1_KeyspaceName).getAddressReplicas(); - - assertRanges(keyspace1ranges, "127.0.0.1", 97, 0); - assertRanges(keyspace1ranges, "127.0.0.2", 0, 10); - assertRanges(keyspace1ranges, "127.0.0.3", 10, 20); - assertRanges(keyspace1ranges, "127.0.0.4", 20, 30); - assertRanges(keyspace1ranges, "127.0.0.5", 30, 40); - assertRanges(keyspace1ranges, "127.0.0.6", 40, 50); - assertRanges(keyspace1ranges, "127.0.0.7", 50, 67); - assertRanges(keyspace1ranges, "127.0.0.8", 67, 70); - assertRanges(keyspace1ranges, "127.0.0.9", 70, 87); - assertRanges(keyspace1ranges, "127.0.0.10", 87, 97); - - - /** - * Keyspace3 RF=5 - * { - * /127.0.0.1=[(97,0], (70,87], (50,67], (87,97], (67,70]], - * /127.0.0.2=[(97,0], (70,87], (87,97], (0,10], (67,70]], - * /127.0.0.3=[(97,0], (70,87], (87,97], (0,10], (10,20]], - * /127.0.0.4=[(97,0], (20,30], (87,97], (0,10], (10,20]], - * /127.0.0.5=[(97,0], (30,40], (20,30], (0,10], (10,20]], - * /127.0.0.6=[(40,50], (30,40], (20,30], (0,10], (10,20]], - * /127.0.0.7=[(40,50], (30,40], (50,67], (20,30], (10,20]], - * /127.0.0.8=[(40,50], (30,40], (50,67], (20,30], (67,70]], - * /127.0.0.9=[(40,50], (70,87], (30,40], (50,67], (67,70]], - * /127.0.0.10=[(40,50], (70,87], (50,67], (87,97], (67,70]] - * } - */ - - RangesByEndpoint keyspace3ranges = keyspaceStrategyMap.get(KEYSPACE3).getAddressReplicas(); - assertRanges(keyspace3ranges, "127.0.0.1", 97, 0, 70, 87, 50, 67, 87, 97, 67, 70); - assertRanges(keyspace3ranges, "127.0.0.2", 97, 0, 70, 87, 87, 97, 0, 10, 67, 70); - assertRanges(keyspace3ranges, "127.0.0.3", 97, 0, 70, 87, 87, 97, 0, 10, 10, 20); - assertRanges(keyspace3ranges, "127.0.0.4", 97, 0, 20, 30, 87, 97, 0, 10, 10, 20); - assertRanges(keyspace3ranges, "127.0.0.5", 97, 0, 30, 40, 20, 30, 0, 10, 10, 20); - assertRanges(keyspace3ranges, "127.0.0.6", 40, 50, 30, 40, 20, 30, 0, 10, 10, 20); - assertRanges(keyspace3ranges, "127.0.0.7", 40, 50, 30, 40, 50, 67, 20, 30, 10, 20); - assertRanges(keyspace3ranges, "127.0.0.8", 40, 50, 30, 40, 50, 67, 20, 30, 67, 70); - assertRanges(keyspace3ranges, "127.0.0.9", 40, 50, 70, 87, 30, 40, 50, 67, 67, 70); - assertRanges(keyspace3ranges, "127.0.0.10", 40, 50, 70, 87, 50, 67, 87, 97, 67, 70); - - - /** - * Keyspace4 RF=3 - * { - * /127.0.0.1=[(97,0], (70,87], (87,97]], - * /127.0.0.2=[(97,0], (87,97], (0,10]], - * /127.0.0.3=[(97,0], (0,10], (10,20]], - * /127.0.0.4=[(20,30], (0,10], (10,20]], - * /127.0.0.5=[(30,40], (20,30], (10,20]], - * /127.0.0.6=[(40,50], (30,40], (20,30]], - * /127.0.0.7=[(40,50], (30,40], (50,67]], - * /127.0.0.8=[(40,50], (50,67], (67,70]], - * /127.0.0.9=[(70,87], (50,67], (67,70]], - * /127.0.0.10=[(70,87], (87,97], (67,70]] - * } - */ - RangesByEndpoint keyspace4ranges = keyspaceStrategyMap.get(Simple_RF3_KeyspaceName).getAddressReplicas(); - - assertRanges(keyspace4ranges, "127.0.0.1", 97, 0, 70, 87, 87, 97); - assertRanges(keyspace4ranges, "127.0.0.2", 97, 0, 87, 97, 0, 10); - assertRanges(keyspace4ranges, "127.0.0.3", 97, 0, 0, 10, 10, 20); - assertRanges(keyspace4ranges, "127.0.0.4", 20, 30, 0, 10, 10, 20); - assertRanges(keyspace4ranges, "127.0.0.5", 30, 40, 20, 30, 10, 20); - assertRanges(keyspace4ranges, "127.0.0.6", 40, 50, 30, 40, 20, 30); - assertRanges(keyspace4ranges, "127.0.0.7", 40, 50, 30, 40, 50, 67); - assertRanges(keyspace4ranges, "127.0.0.8", 40, 50, 50, 67, 67, 70); - assertRanges(keyspace4ranges, "127.0.0.9", 70, 87, 50, 67, 67, 70); - assertRanges(keyspace4ranges, "127.0.0.10", 70, 87, 87, 97, 67, 70); - - // pre-calculate the results. - Map> expectedEndpoints = new HashMap<>(); - expectedEndpoints.put(Simple_RF1_KeyspaceName, HashMultimap.create()); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.1.1")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.7")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.1.2")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.9")); - expectedEndpoints.get(Simple_RF1_KeyspaceName).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.10")); - expectedEndpoints.put(KEYSPACE2, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.7")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.9")); - expectedEndpoints.get(KEYSPACE2).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.10")); - expectedEndpoints.put(KEYSPACE3, HashMultimap.create()); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.1.1")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.1.2")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3")); - expectedEndpoints.get(KEYSPACE3).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.10", "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4")); - expectedEndpoints.put(Simple_RF3_KeyspaceName, HashMultimap.create()); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("5"), makeAddrs("127.0.0.2", "127.0.0.3", "127.0.0.4")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("15"), makeAddrs("127.0.0.3", "127.0.0.4", "127.0.0.5")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("25"), makeAddrs("127.0.0.4", "127.0.0.5", "127.0.0.6")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("35"), makeAddrs("127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.1.1")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("45"), makeAddrs("127.0.0.6", "127.0.0.7", "127.0.0.8", "127.0.1.1")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("55"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.1.1", "127.0.1.2")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("65"), makeAddrs("127.0.0.7", "127.0.0.8", "127.0.0.9", "127.0.1.2")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("75"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1", "127.0.1.2")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("85"), makeAddrs("127.0.0.9", "127.0.0.10", "127.0.0.1")); - expectedEndpoints.get(Simple_RF3_KeyspaceName).putAll(new BigIntegerToken("95"), makeAddrs("127.0.0.10", "127.0.0.1", "127.0.0.2")); - - for (Map.Entry keyspaceStrategy : keyspaceStrategyMap.entrySet()) - { - String keyspaceName = keyspaceStrategy.getKey(); - AbstractReplicationStrategy strategy = keyspaceStrategy.getValue(); - - for (Token token : keyTokens) - { - Collection endpoints = tmd.getWriteEndpoints(token, keyspaceName, strategy.getNaturalReplicasForToken(token)).endpoints(); - assertEquals(expectedEndpoints.get(keyspaceName).get(token).size(), endpoints.size()); - assertTrue(expectedEndpoints.get(keyspaceName).get(token).containsAll(endpoints)); - } - - // just to be sure that things still work according to the old tests, run them: - if (strategy.getReplicationFactor().allReplicas != 3) - continue; - - ReplicaCollection replicas = null; - // tokens 5, 15 and 25 should go three nodes - for (int i = 0; i < 3; i++) - { - replicas = tmd.getWriteEndpoints(keyTokens.get(i), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(i))); - assertEquals(3, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(i + 1))); - assertTrue(replicas.endpoints().contains(hosts.get(i + 2))); - assertTrue(replicas.endpoints().contains(hosts.get(i + 3))); - } - - // token 35 should go to nodes 4, 5, 6 and boot1 - replicas = tmd.getWriteEndpoints(keyTokens.get(3), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(3))); - assertEquals(4, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(4))); - assertTrue(replicas.endpoints().contains(hosts.get(5))); - assertTrue(replicas.endpoints().contains(hosts.get(6))); - assertTrue(replicas.endpoints().contains(boot1)); - - // token 45 should go to nodes 5, 6, 7 boot1 - replicas = tmd.getWriteEndpoints(keyTokens.get(4), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(4))); - assertEquals(4, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(5))); - assertTrue(replicas.endpoints().contains(hosts.get(6))); - assertTrue(replicas.endpoints().contains(hosts.get(7))); - assertTrue(replicas.endpoints().contains(boot1)); - - // token 55 should go to nodes 6, 7, 8 boot1 and boot2 - replicas = tmd.getWriteEndpoints(keyTokens.get(5), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(5))); - assertEquals(5, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(6))); - assertTrue(replicas.endpoints().contains(hosts.get(7))); - assertTrue(replicas.endpoints().contains(hosts.get(8))); - assertTrue(replicas.endpoints().contains(boot1)); - assertTrue(replicas.endpoints().contains(boot2)); - - // token 65 should go to nodes 6, 7, 8 and boot2 - replicas = tmd.getWriteEndpoints(keyTokens.get(6), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(6))); - assertEquals(4, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(6))); - assertTrue(replicas.endpoints().contains(hosts.get(7))); - assertTrue(replicas.endpoints().contains(hosts.get(8))); - assertTrue(replicas.endpoints().contains(boot2)); - - // token 75 should to go nodes 8, 9, 0 and boot2 - replicas = tmd.getWriteEndpoints(keyTokens.get(7), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(7))); - assertEquals(4, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(8))); - assertTrue(replicas.endpoints().contains(hosts.get(9))); - assertTrue(replicas.endpoints().contains(hosts.get(0))); - assertTrue(replicas.endpoints().contains(boot2)); - - // token 85 should go to nodes 8, 9 and 0 - replicas = tmd.getWriteEndpoints(keyTokens.get(8), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(8))); - assertEquals(3, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(8))); - assertTrue(replicas.endpoints().contains(hosts.get(9))); - assertTrue(replicas.endpoints().contains(hosts.get(0))); - - // token 95 should go to nodes 9, 0 and 1 - replicas = tmd.getWriteEndpoints(keyTokens.get(9), keyspaceName, strategy.getNaturalReplicasForToken(keyTokens.get(9))); - assertEquals(3, replicas.size()); - assertTrue(replicas.endpoints().contains(hosts.get(9))); - assertTrue(replicas.endpoints().contains(hosts.get(0))); - assertTrue(replicas.endpoints().contains(hosts.get(1))); - } - - // all moving nodes are back to the normal state - for (Integer movingIndex : MOVING) - { - ss.onChange(hosts.get(movingIndex), - ApplicationState.STATUS, - valueFactory.normal(Collections.singleton(newTokens.get(movingIndex)))); - } - } - - @Test - public void testStateJumpToNormal() throws UnknownHostException - { - StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); - IPartitioner partitioner = RandomPartitioner.instance; - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(partitioner); - - ArrayList endpointTokens = new ArrayList(); - ArrayList keyTokens = new ArrayList(); - List hosts = new ArrayList<>(); - List hostIds = new ArrayList(); - - // create a ring or 6 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 6); - - // node 2 leaves - Token newToken = positionToken(7); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.moving(newToken)); - - assertTrue(tmd.isMoving(hosts.get(2))); - assertEquals(endpointTokens.get(2), tmd.getTokens(hosts.get(2)).iterator().next()); - - // back to normal - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(newToken))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(newToken))); - - assertTrue(tmd.getSizeOfMovingEndpoints() == 0); - assertEquals(newToken, tmd.getTokens(hosts.get(2)).iterator().next()); - - newToken = positionToken(8); - // node 2 goes through leave and left and then jumps to normal at its new token - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.moving(newToken)); - Gossiper.instance.injectApplicationState(hosts.get(2), ApplicationState.TOKENS, valueFactory.tokens(Collections.singleton(newToken))); - ss.onChange(hosts.get(2), ApplicationState.STATUS, valueFactory.normal(Collections.singleton(newToken))); - - assertTrue(tmd.getBootstrapTokens().isEmpty()); - assertTrue(tmd.getSizeOfMovingEndpoints() == 0); - assertEquals(newToken, tmd.getTokens(hosts.get(2)).iterator().next()); - } - - private static Collection makeAddrs(String... hosts) throws UnknownHostException - { - ArrayList addrs = new ArrayList<>(hosts.length); - for (String host : hosts) - addrs.add(InetAddressAndPort.getByName(host)); - return addrs; - } - - private static EndpointsForRange makeReplicas(Range range, String... hosts) throws UnknownHostException - { - EndpointsForRange.Builder replicas = EndpointsForRange.builder(range, hosts.length); - for (String host : hosts) - replicas.add(Replica.fullReplica(InetAddressAndPort.getByName(host), range)); - return replicas.build(); - } - - private AbstractReplicationStrategy getStrategy(String keyspaceName, TokenMetadata tmd) - { - KeyspaceMetadata ksmd = Schema.instance.getKeyspaceMetadata(keyspaceName); - return AbstractReplicationStrategy.createReplicationStrategy( - keyspaceName, - ksmd.params.replication.klass, - tmd, - new SimpleSnitch(), - ksmd.params.replication.options); - } - - private Token positionToken(int position) - { - return new BigIntegerToken(String.valueOf(10 * position + 7)); - } - - private static int collectionSize(Collection collection) - { - if (collection.isEmpty()) - return 0; - - Iterator iterator = collection.iterator(); - - int count = 0; - while (iterator.hasNext()) - { - iterator.next(); - count++; - } - - return count; - } - - private Collection> generateRanges(int... rangePairs) - { - if (rangePairs.length % 2 == 1) - throw new RuntimeException("generateRanges argument count should be even"); - - Set> ranges = new HashSet>(); - - for (int i = 0; i < rangePairs.length; i+=2) - { - ranges.add(generateRange(rangePairs[i], rangePairs[i+1])); - } - - return ranges; - } - - private static Token tk(int v) - { - return new BigIntegerToken(String.valueOf(v)); - } - - private static Range generateRange(int left, int right) - { - return new Range(tk(left), tk(right)); - } - - private static Replica replica(InetAddressAndPort endpoint, int left, int right, boolean full) - { - return new Replica(endpoint, tk(left), tk(right), full); - } - - private static InetAddressAndPort inet(String name) - { - try - { - return InetAddressAndPort.getByName(name); - } - catch (UnknownHostException e) - { - throw new AssertionError(e); - } - } - - private static Replica replica(InetAddressAndPort endpoint, int left, int right) - { - return replica(endpoint, left, right, true); - } - - private static void assertRanges(RangesByEndpoint epReplicas, String endpoint, int... rangePairs) - { - if (rangePairs.length % 2 == 1) - throw new RuntimeException("assertRanges argument count should be even"); - - InetAddressAndPort ep = inet(endpoint); - List expected = new ArrayList<>(rangePairs.length/2); - for (int i=0; i downNodes = new ArrayList<>(); - - final RangeStreamer.SourceFilter alivePredicate = new RangeStreamer.SourceFilter() - { - public boolean apply(Replica replica) - { - return !downNodes.contains(replica.endpoint()); - } - - public String message(Replica replica) - { - return "Down nodes: " + downNodes; - } - }; - - final RangeStreamer.SourceFilter sourceFilterDownNodesPredicate = new RangeStreamer.SourceFilter() - { - public boolean apply(Replica replica) - { - return !sourceFilterDownNodes.contains(replica.endpoint()); - } - - public String message(Replica replica) - { - return "Source filter down nodes: " + sourceFilterDownNodes; - } - }; - - private final List sourceFilterDownNodes = new ArrayList<>(); - - private final Collection sourceFilters = Arrays.asList(alivePredicate, - sourceFilterDownNodesPredicate, - new RangeStreamer.ExcludeLocalNodeFilter() - ); - - @After - public void clearDownNode() - { - downNodes.clear(); - sourceFilterDownNodes.clear(); - } - - @BeforeClass - public static void setupDD() - { - DatabaseDescriptor.daemonInitialization(); - } - - final Token oneToken = new RandomPartitioner.BigIntegerToken("1"); - final Token twoToken = new RandomPartitioner.BigIntegerToken("2"); - final Token threeToken = new RandomPartitioner.BigIntegerToken("3"); - final Token fourToken = new RandomPartitioner.BigIntegerToken("4"); - final Token sixToken = new RandomPartitioner.BigIntegerToken("6"); - final Token sevenToken = new RandomPartitioner.BigIntegerToken("7"); - final Token nineToken = new RandomPartitioner.BigIntegerToken("9"); - final Token elevenToken = new RandomPartitioner.BigIntegerToken("11"); - final Token fourteenToken = new RandomPartitioner.BigIntegerToken("14"); - - final Range range_1_2 = new Range(oneToken, threeToken); - final Range range_3_6 = new Range(threeToken, sixToken); - final Range range_6_9 = new Range(sixToken, nineToken); - final Range range_9_11 = new Range(nineToken, elevenToken); - final Range range_11_1 = new Range(elevenToken, oneToken); - - - final RangesAtEndpoint current = RangesAtEndpoint.of(new Replica(address01, range_1_2, true), - new Replica(address01, range_11_1, true), - new Replica(address01, range_9_11, false)); - - public Token token(String s) - { - return new RandomPartitioner.BigIntegerToken(s); - } - - public Range range(String start, String end) - { - return new Range<>(token(start), token(end)); - } - - /** - * Ring with start A 1-3 B 3-6 C 6-9 D 9-1 - * A's token moves from 3 to 4. - *

    - * Result is A gains some range - * - * @throws Exception - */ - @Test - public void testCalculateStreamAndFetchRangesMoveForward() throws Exception - { - calculateStreamAndFetchRangesMoveForward(); - } - - private Pair calculateStreamAndFetchRangesMoveForward() throws Exception - { - Range aPrimeRange = new Range<>(oneToken, fourToken); - - RangesAtEndpoint updated = RangesAtEndpoint.of( - new Replica(address01, aPrimeRange, true), - new Replica(address01, range_11_1, true), - new Replica(address01, range_9_11, false) - ); - - Pair result = RangeRelocator.calculateStreamAndFetchRanges(current, updated); - assertContentsIgnoreOrder(result.left); - assertContentsIgnoreOrder(result.right, fullReplica(address01, threeToken, fourToken)); - return result; - } - - /** - * Ring with start A 1-3 B 3-6 C 6-9 D 9-11 E 11-1 - * A's token moves from 3 to 14 - *

    - * Result is A loses range and it must be streamed - * - * @throws Exception - */ - @Test - public void testCalculateStreamAndFetchRangesMoveBackwardBetween() throws Exception - { - calculateStreamAndFetchRangesMoveBackwardBetween(); - } - - public Pair calculateStreamAndFetchRangesMoveBackwardBetween() throws Exception - { - Range aPrimeRange = new Range<>(elevenToken, fourteenToken); - - RangesAtEndpoint updated = RangesAtEndpoint.of( - new Replica(address01, aPrimeRange, true), - new Replica(address01, range_9_11, true), - new Replica(address01, range_6_9, false) - ); - - - Pair result = RangeRelocator.calculateStreamAndFetchRanges(current, updated); - assertContentsIgnoreOrder(result.left, fullReplica(address01, oneToken, threeToken), fullReplica(address01, fourteenToken, oneToken)); - assertContentsIgnoreOrder(result.right, transientReplica(address01, sixToken, nineToken), fullReplica(address01, nineToken, elevenToken)); - return result; - } - - /** - * Ring with start A 1-3 B 3-6 C 6-9 D 9-11 E 11-1 - * A's token moves from 3 to 2 - * - * Result is A loses range and it must be streamed - * @throws Exception - */ - @Test - public void testCalculateStreamAndFetchRangesMoveBackward() throws Exception - { - calculateStreamAndFetchRangesMoveBackward(); - } - - private Pair calculateStreamAndFetchRangesMoveBackward() throws Exception - { - Range aPrimeRange = new Range<>(oneToken, twoToken); - - RangesAtEndpoint updated = RangesAtEndpoint.of( - new Replica(address01, aPrimeRange, true), - new Replica(address01, range_11_1, true), - new Replica(address01, range_9_11, false) - ); - - Pair result = RangeRelocator.calculateStreamAndFetchRanges(current, updated); - - //Moving backwards has no impact on any replica. We already fully replicate counter clockwise - //The transient replica does transiently replicate slightly more, but that is addressed by cleanup - assertContentsIgnoreOrder(result.left, fullReplica(address01, twoToken, threeToken)); - assertContentsIgnoreOrder(result.right); - - return result; - } - - /** - * Ring with start A 1-3 B 3-6 C 6-9 D 9-11 E 11-1 - * A's moves from 3 to 7 - * - * @throws Exception - */ - private Pair calculateStreamAndFetchRangesMoveForwardBetween() throws Exception - { - Range aPrimeRange = new Range<>(sixToken, sevenToken); - Range bPrimeRange = new Range<>(oneToken, sixToken); - - RangesAtEndpoint updated = RangesAtEndpoint.of( - new Replica(address01, aPrimeRange, true), - new Replica(address01, bPrimeRange, true), - new Replica(address01, range_11_1, false) - ); - - Pair result = RangeRelocator.calculateStreamAndFetchRanges(current, updated); - - assertContentsIgnoreOrder(result.left, fullReplica(address01, elevenToken, oneToken), transientReplica(address01, nineToken, elevenToken)); - assertContentsIgnoreOrder(result.right, fullReplica(address01, threeToken, sixToken), fullReplica(address01, sixToken, sevenToken)); - return result; - } - - /** - * Ring with start A 1-3 B 3-6 C 6-9 D 9-11 E 11-1 - * A's token moves from 3 to 7 - * - * @throws Exception - */ - @Test - public void testCalculateStreamAndFetchRangesMoveForwardBetween() throws Exception - { - calculateStreamAndFetchRangesMoveForwardBetween(); - } - - @Test - public void testResubtract() - { - Token oneToken = new RandomPartitioner.BigIntegerToken("0001"); - Token tenToken = new RandomPartitioner.BigIntegerToken("0010"); - Token fiveToken = new RandomPartitioner.BigIntegerToken("0005"); - - Range range_1_10 = new Range<>(oneToken, tenToken); - Range range_1_5 = new Range<>(oneToken, tenToken); - Range range_5_10 = new Range<>(fiveToken, tenToken); - - RangesAtEndpoint singleRange = RangesAtEndpoint.of( - new Replica(address01, range_1_10, true) - ); - - RangesAtEndpoint splitRanges = RangesAtEndpoint.of( - new Replica(address01, range_1_5, true), - new Replica(address01, range_5_10, true) - ); - - // forward - Pair calculated = RangeRelocator.calculateStreamAndFetchRanges(singleRange, splitRanges); - assertTrue(calculated.left.toString(), calculated.left.isEmpty()); - assertTrue(calculated.right.toString(), calculated.right.isEmpty()); - - // backward - calculated = RangeRelocator.calculateStreamAndFetchRanges(splitRanges, singleRange); - assertTrue(calculated.left.toString(), calculated.left.isEmpty()); - assertTrue(calculated.right.toString(), calculated.right.isEmpty()); - } - - /** - * Construct the ring state for calculateStreamAndFetchRangesMoveBackwardBetween - * Where are A moves from 3 to 14 - * @return - */ - private Pair constructTMDsMoveBackwardBetween() - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(range_1_2.right, address01); - tmd.updateNormalToken(range_3_6.right, address02); - tmd.updateNormalToken(range_6_9.right, address03); - tmd.updateNormalToken(range_9_11.right, address04); - tmd.updateNormalToken(range_11_1.right, address05); - tmd.addMovingEndpoint(fourteenToken, address01); - TokenMetadata updated = tmd.cloneAfterAllSettled(); - - return Pair.create(tmd, updated); - } - - - /** - * Construct the ring state for calculateStreamAndFetchRangesMoveForwardBetween - * Where are A moves from 3 to 7 - * @return - */ - private Pair constructTMDsMoveForwardBetween() - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(range_1_2.right, address01); - tmd.updateNormalToken(range_3_6.right, address02); - tmd.updateNormalToken(range_6_9.right, address03); - tmd.updateNormalToken(range_9_11.right, address04); - tmd.updateNormalToken(range_11_1.right, address05); - tmd.addMovingEndpoint(sevenToken, address01); - TokenMetadata updated = tmd.cloneAfterAllSettled(); - - return Pair.create(tmd, updated); - } - - private Pair constructTMDsMoveBackward() - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(range_1_2.right, address01); - tmd.updateNormalToken(range_3_6.right, address02); - tmd.updateNormalToken(range_6_9.right, address03); - tmd.updateNormalToken(range_9_11.right, address04); - tmd.updateNormalToken(range_11_1.right, address05); - tmd.addMovingEndpoint(twoToken, address01); - TokenMetadata updated = tmd.cloneAfterAllSettled(); - - return Pair.create(tmd, updated); - } - - private Pair constructTMDsMoveForward() - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(range_1_2.right, address01); - tmd.updateNormalToken(range_3_6.right, address02); - tmd.updateNormalToken(range_6_9.right, address03); - tmd.updateNormalToken(range_9_11.right, address04); - tmd.updateNormalToken(range_11_1.right, address05); - tmd.addMovingEndpoint(fourToken, address01); - TokenMetadata updated = tmd.cloneAfterAllSettled(); - - return Pair.create(tmd, updated); - } - - - @Test - public void testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints() throws Exception - { - EndpointsByReplica.Builder expectedResult = new EndpointsByReplica.Builder(); - - InetAddressAndPort cOrB = (downNodes.contains(address03) || sourceFilterDownNodes.contains(address03)) ? address02 : address03; - - //Need to pull the full replica and the transient replica that is losing the range - expectedResult.put(fullReplica(address01, sixToken, sevenToken), fullReplica(address04, sixToken, nineToken)); - expectedResult.put(fullReplica(address01, sixToken, sevenToken), transientReplica(address05, sixToken, nineToken)); - - //Same need both here as well - expectedResult.put(fullReplica(address01, threeToken, sixToken), fullReplica(cOrB, threeToken, sixToken)); - expectedResult.put(fullReplica(address01, threeToken, sixToken), transientReplica(address04, threeToken, sixToken)); - - invokeCalculateRangesToFetchWithPreferredEndpoints(calculateStreamAndFetchRangesMoveForwardBetween().right, - constructTMDsMoveForwardBetween(), - expectedResult.build()); - } - - @Test - public void testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpointsDownNodes() throws Exception - { - for (InetAddressAndPort downNode : new InetAddressAndPort[] { address04, address05 }) - { - downNodes.clear(); - downNodes.add(downNode); - boolean threw = false; - try - { - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - catch (IllegalStateException ise) - { - ise.printStackTrace(); - assertTrue(downNode.toString(), - ise.getMessage().contains("Down nodes: [" + downNode + "]")); - threw = true; - } - assertTrue("Didn't throw for " + downNode, threw); - } - - //Shouldn't throw because another full replica is available - downNodes.clear(); - downNodes.add(address03); - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - @Test - public void testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpointsDownNodesSourceFilter() throws Exception - { - for (InetAddressAndPort downNode : new InetAddressAndPort[] { address04, address05 }) - { - sourceFilterDownNodes.clear(); - sourceFilterDownNodes.add(downNode); - boolean threw = false; - try - { - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - catch (IllegalStateException ise) - { - ise.printStackTrace(); - assertTrue(downNode.toString(), - ise.getMessage().startsWith("Necessary replicas for strict consistency were removed by source filters:") - && ise.getMessage().contains(downNode.toString())); - threw = true; - } - assertTrue("Didn't throw for " + downNode, threw); - } - - //Shouldn't throw because another full replica is available - sourceFilterDownNodes.clear(); - sourceFilterDownNodes.add(address03); - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - @Test - public void testMoveBackwardBetweenCalculateRangesToFetchWithPreferredEndpoints() throws Exception - { - EndpointsByReplica.Builder expectedResult = new EndpointsByReplica.Builder(); - - //Need to pull the full replica and the transient replica that is losing the range - expectedResult.put(fullReplica(address01, nineToken, elevenToken), fullReplica(address05, nineToken, elevenToken)); - expectedResult.put(transientReplica(address01, sixToken, nineToken), transientReplica(address05, sixToken, nineToken)); - - invokeCalculateRangesToFetchWithPreferredEndpoints(calculateStreamAndFetchRangesMoveBackwardBetween().right, - constructTMDsMoveBackwardBetween(), - expectedResult.build()); - - } - - @Test(expected = IllegalStateException.class) - public void testMoveBackwardBetweenCalculateRangesToFetchWithPreferredEndpointsDownNodes() throws Exception - { - //Any replica can be the full replica so this will always fail on the transient range - downNodes.add(address05); - testMoveBackwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - @Test(expected = IllegalStateException.class) - public void testMoveBackwardBetweenCalculateRangesToFetchWithPreferredEndpointsDownNodesSourceFilter() throws Exception - { - //Any replica can be the full replica so this will always fail on the transient range - sourceFilterDownNodes.add(address05); - testMoveBackwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - - //There is no down node version of this test because nothing needs to be fetched - @Test - public void testMoveBackwardCalculateRangesToFetchWithPreferredEndpoints() throws Exception - { - //Moving backwards should fetch nothing and fetch ranges is emptys so this doesn't test a ton - EndpointsByReplica.Builder expectedResult = new EndpointsByReplica.Builder(); - - invokeCalculateRangesToFetchWithPreferredEndpoints(calculateStreamAndFetchRangesMoveBackward().right, - constructTMDsMoveBackward(), - expectedResult.build()); - - } - - @Test - public void testMoveForwardCalculateRangesToFetchWithPreferredEndpoints() throws Exception - { - EndpointsByReplica.Builder expectedResult = new EndpointsByReplica.Builder(); - - InetAddressAndPort cOrBAddress = (downNodes.contains(address03) || sourceFilterDownNodes.contains(address03)) ? address02 : address03; - - //Need to pull the full replica and the transient replica that is losing the range - expectedResult.put(fullReplica(address01, threeToken, fourToken), fullReplica(cOrBAddress, threeToken, sixToken)); - expectedResult.put(fullReplica(address01, threeToken, fourToken), transientReplica(address04, threeToken, sixToken)); - - invokeCalculateRangesToFetchWithPreferredEndpoints(calculateStreamAndFetchRangesMoveForward().right, - constructTMDsMoveForward(), - expectedResult.build()); - - } - - @Test - public void testMoveForwardCalculateRangesToFetchWithPreferredEndpointsDownNodes() throws Exception - { - downNodes.add(address04); - boolean threw = false; - try - { - testMoveForwardCalculateRangesToFetchWithPreferredEndpoints(); - } - catch (IllegalStateException ise) - { - ise.printStackTrace(); - assertTrue(address04.toString(), - ise.getMessage().contains("Down nodes: [" + address04 + "]")); - threw = true; - } - assertTrue("Didn't throw for " + address04, threw); - - //Shouldn't throw because another full replica is available - downNodes.clear(); - downNodes.add(address03); - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - @Test - public void testMoveForwardCalculateRangesToFetchWithPreferredEndpointsDownNodesSourceFilter() throws Exception - { - sourceFilterDownNodes.add(address04); - boolean threw = false; - try - { - testMoveForwardCalculateRangesToFetchWithPreferredEndpoints(); - } - catch (IllegalStateException ise) - { - ise.printStackTrace(); - assertTrue(address04.toString(), - ise.getMessage().startsWith("Necessary replicas for strict consistency were removed by source filters:") - && ise.getMessage().contains(address04.toString())); - threw = true; - } - assertTrue("Didn't throw for " + address04, threw); - - //Shouldn't throw because another full replica is available - sourceFilterDownNodes.clear(); - sourceFilterDownNodes.add(address03); - testMoveForwardBetweenCalculateRangesToFetchWithPreferredEndpoints(); - } - - private void invokeCalculateRangesToFetchWithPreferredEndpoints(RangesAtEndpoint toFetch, - Pair tmds, - EndpointsByReplica expectedResult) - { - DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - - EndpointsByReplica result = RangeStreamer.calculateRangesToFetchWithPreferredEndpoints((address, replicas) -> replicas.sorted((a, b) -> b.endpoint().compareTo(a.endpoint())), - simpleStrategy(tmds.left), - toFetch, - true, - tmds.left, - tmds.right, - "TestKeyspace", - sourceFilters); - logger.info("Ranges to fetch with preferred endpoints"); - logger.info(result.toString()); - assertMultimapEqualsIgnoreOrder(expectedResult, result); - } - - private AbstractReplicationStrategy simpleStrategy(TokenMetadata tmd) - { - IEndpointSnitch snitch = new AbstractEndpointSnitch() - { - public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) - { - return 0; - } - - public String getRack(InetAddressAndPort endpoint) - { - return "R1"; - } - - public String getDatacenter(InetAddressAndPort endpoint) - { - return "DC1"; - } - }; - - return new SimpleStrategy("MoveTransientTest", - tmd, - snitch, - com.google.common.collect.ImmutableMap.of("replication_factor", "3/1")); - } - - @Test - public void testMoveForwardBetweenCalculateRangesToStreamWithPreferredEndpoints() throws Exception - { - DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - RangesByEndpoint.Builder expectedResult = new RangesByEndpoint.Builder(); - - //Need to pull the full replica and the transient replica that is losing the range - expectedResult.put(address02, transientReplica(address02, nineToken, elevenToken)); - expectedResult.put(address02, fullReplica(address02, elevenToken, oneToken)); - - invokeCalculateRangesToStreamWithPreferredEndpoints(calculateStreamAndFetchRangesMoveForwardBetween().left, - constructTMDsMoveForwardBetween(), - expectedResult.build()); - } - - @Test - public void testMoveBackwardBetweenCalculateRangesToStreamWithPreferredEndpoints() throws Exception - { - RangesByEndpoint.Builder expectedResult = new RangesByEndpoint.Builder(); - - expectedResult.put(address02, fullReplica(address02, fourteenToken, oneToken)); - - expectedResult.put(address04, transientReplica(address04, oneToken, threeToken)); - - expectedResult.put(address03, fullReplica(address03, oneToken, threeToken)); - expectedResult.put(address03, transientReplica(address03, fourteenToken, oneToken)); - - invokeCalculateRangesToStreamWithPreferredEndpoints(calculateStreamAndFetchRangesMoveBackwardBetween().left, - constructTMDsMoveBackwardBetween(), - expectedResult.build()); - } - - @Test - public void testMoveBackwardCalculateRangesToStreamWithPreferredEndpoints() throws Exception - { - RangesByEndpoint.Builder expectedResult = new RangesByEndpoint.Builder(); - expectedResult.put(address03, fullReplica(address03, twoToken, threeToken)); - expectedResult.put(address04, transientReplica(address04, twoToken, threeToken)); - - invokeCalculateRangesToStreamWithPreferredEndpoints(calculateStreamAndFetchRangesMoveBackward().left, - constructTMDsMoveBackward(), - expectedResult.build()); - } - - @Test - public void testMoveForwardCalculateRangesToStreamWithPreferredEndpoints() throws Exception - { - //Nothing to stream moving forward because we are acquiring more range not losing range - RangesByEndpoint.Builder expectedResult = new RangesByEndpoint.Builder(); - - invokeCalculateRangesToStreamWithPreferredEndpoints(calculateStreamAndFetchRangesMoveForward().left, - constructTMDsMoveForward(), - expectedResult.build()); - } - - private void invokeCalculateRangesToStreamWithPreferredEndpoints(RangesAtEndpoint toStream, - Pair tmds, - RangesByEndpoint expectedResult) - { - DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - RangeRelocator relocator = new RangeRelocator(); - RangesByEndpoint result = relocator.calculateRangesToStreamWithEndpoints(toStream, - simpleStrategy(tmds.left), - tmds.left, - tmds.right); - logger.info("Ranges to stream by endpoint"); - logger.info(result.toString()); - assertMultimapEqualsIgnoreOrder(expectedResult, result); - } - - private static void assertContentsIgnoreOrder(RangesAtEndpoint ranges, Replica ... replicas) - { - assertEquals(ranges.size(), replicas.length); - for (Replica replica : replicas) - { - if (!ranges.contains(replica)) - assertTrue(Iterables.elementsEqual(RangesAtEndpoint.of(replicas), ranges)); - } - } - -} diff --git a/test/unit/org/apache/cassandra/service/PartitionDenylistTest.java b/test/unit/org/apache/cassandra/service/PartitionDenylistTest.java index 4a93c67bfe..e17dc2af1b 100644 --- a/test/unit/org/apache/cassandra/service/PartitionDenylistTest.java +++ b/test/unit/org/apache/cassandra/service/PartitionDenylistTest.java @@ -18,6 +18,7 @@ package org.apache.cassandra.service; + import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -27,14 +28,10 @@ import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.UntypedResultSet; -import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.schema.SchemaTestUtil; -import org.apache.cassandra.schema.Tables; +import static org.apache.cassandra.cql3.CQLTester.requireNetwork; import static org.apache.cassandra.cql3.QueryProcessor.process; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -48,36 +45,33 @@ public class PartitionDenylistTest ServerTestUtils.daemonInitialization(); CQLTester.prepareServer(); + process("create keyspace "+ks_cql+" with replication = {'class':'SimpleStrategy', 'replication_factor':1}", ConsistencyLevel.ONE); + process("CREATE TABLE "+ks_cql+".table1 (" + + "keyone text, " + + "keytwo text, " + + "qux text, " + + "quz text, " + + "foo text, " + + "PRIMARY KEY((keyone, keytwo), qux, quz) ) ", ConsistencyLevel.ONE); - KeyspaceMetadata schema = KeyspaceMetadata.create(ks_cql, - KeyspaceParams.simple(1), - Tables.of( - CreateTableStatement.parse("CREATE TABLE table1 (" - + "keyone text, " - + "keytwo text, " - + "qux text, " - + "quz text, " - + "foo text, " - + "PRIMARY KEY((keyone, keytwo), qux, quz) ) ", ks_cql).build(), - CreateTableStatement.parse("CREATE TABLE table2 (" - + "keyone text, " - + "keytwo text, " - + "keythree text, " - + "value text, " - + "PRIMARY KEY((keyone, keytwo), keythree) ) ", ks_cql).build(), - CreateTableStatement.parse("CREATE TABLE table3 (" - + "keyone text, " - + "keytwo text, " - + "keythree text, " - + "value text, " - + "PRIMARY KEY((keyone, keytwo), keythree) ) ", ks_cql).build() - )); - SchemaTestUtil.addOrUpdateKeyspace(schema, false); + process("CREATE TABLE "+ks_cql+".table2 (" + + "keyone text, " + + "keytwo text, " + + "keythree text, " + + "value text, " + + "PRIMARY KEY((keyone, keytwo), keythree) ) ", ConsistencyLevel.ONE); + + process("CREATE TABLE "+ks_cql+".table3 (" + + "keyone text, " + + "keytwo text, " + + "keythree text, " + + "value text, " + + "PRIMARY KEY((keyone, keytwo), keythree) ) ", ConsistencyLevel.ONE); DatabaseDescriptor.setPartitionDenylistEnabled(true); DatabaseDescriptor.setDenylistRangeReadsEnabled(true); DatabaseDescriptor.setDenylistConsistencyLevel(ConsistencyLevel.ONE); DatabaseDescriptor.setDenylistRefreshSeconds(1); - StorageService.instance.initServer(0); + requireNetwork(); } @Before diff --git a/test/unit/org/apache/cassandra/service/RemoveTest.java b/test/unit/org/apache/cassandra/service/RemoveTest.java index acc54cbe9c..b2c664ebf2 100644 --- a/test/unit/org/apache/cassandra/service/RemoveTest.java +++ b/test/unit/org/apache/cassandra/service/RemoveTest.java @@ -21,35 +21,29 @@ package org.apache.cassandra.service; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; -import org.apache.cassandra.concurrent.NamedThreadFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.VersionedValue.VersionedValueFactory; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.tcm.membership.NodeId; -import static org.apache.cassandra.net.NoPayload.noPayload; -import static org.apache.cassandra.net.Verb.REPLICATION_DONE_REQ; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.apache.cassandra.tcm.membership.MembershipUtils.*; public class RemoveTest { @@ -61,7 +55,6 @@ public class RemoveTest static final IPartitioner partitioner = RandomPartitioner.instance; StorageService ss = StorageService.instance; - TokenMetadata tmd = ss.getTokenMetadata(); static IPartitioner oldPartitioner; ArrayList endpointTokens = new ArrayList(); ArrayList keyTokens = new ArrayList(); @@ -74,6 +67,7 @@ public class RemoveTest public static void setupClass() throws ConfigurationException { oldPartitioner = StorageService.instance.setPartitionerUnsafe(partitioner); + ServerTestUtils.prepareServerNoRegister(); MessagingService.instance().listen(); } @@ -86,10 +80,10 @@ public class RemoveTest @Before public void setup() throws IOException, ConfigurationException { - tmd.clearUnsafe(); + ServerTestUtils.resetCMS(); // create a ring of 5 nodes - Util.createInitialRing(ss, partitioner, endpointTokens, keyTokens, hosts, hostIds, 6); + Util.createInitialRing(endpointTokens, keyTokens, hosts, hostIds, 6); removalhost = hosts.get(5); hosts.remove(removalhost); @@ -97,14 +91,6 @@ public class RemoveTest hostIds.remove(removalId); } - @After - public void tearDown() - { - MessagingService.instance().inboundSink.clear(); - MessagingService.instance().outboundSink.clear(); - MessagingService.instance().callbacks.unsafeClear(); - } - @Test(expected = UnsupportedOperationException.class) public void testBadHostId() { @@ -123,58 +109,10 @@ public class RemoveTest public void testNonmemberId() { VersionedValueFactory valueFactory = new VersionedValueFactory(DatabaseDescriptor.getPartitioner()); - Collection tokens = Collections.singleton(DatabaseDescriptor.getPartitioner().getRandomToken()); - InetAddressAndPort joininghost = hosts.get(4); - UUID joiningId = hostIds.get(4); - - hosts.remove(joininghost); - hostIds.remove(joiningId); - - // Change a node to a bootstrapping node that is not yet a member of the ring - Gossiper.instance.injectApplicationState(joininghost, ApplicationState.TOKENS, valueFactory.tokens(tokens)); - ss.onChange(joininghost, ApplicationState.STATUS, valueFactory.bootstrapping(tokens)); - - ss.removeNode(joiningId.toString()); - } - - @Test - public void testRemoveHostId() throws InterruptedException - { - // start removal in background and send replication confirmations - final AtomicBoolean success = new AtomicBoolean(false); - Thread remover = NamedThreadFactory.createAnonymousThread(() -> - { - try - { - ss.removeNode(removalId.toString()); - } - catch (Exception e) - { - System.err.println(e); - e.printStackTrace(); - return; - } - success.set(true); - }); - remover.start(); - - Thread.sleep(1000); // make sure removal is waiting for confirmation - - assertTrue(tmd.isLeaving(removalhost)); - assertEquals(1, tmd.getSizeOfLeavingEndpoints()); - - for (InetAddressAndPort host : hosts) - { - Message msg = Message.builder(REPLICATION_DONE_REQ, noPayload) - .from(host) - .build(); - MessagingService.instance().send(msg, FBUtilities.getBroadcastAddressAndPort()); - } - - remover.join(); - - assertTrue(success.get()); - assertTrue(tmd.getSizeOfLeavingEndpoints() == 0); + InetAddressAndPort joiningEndpoint = endpoint(99); + NodeId joiningId = ClusterMetadataTestHelper.register(joiningEndpoint); + ClusterMetadataTestHelper.joinPartially(joiningEndpoint, DatabaseDescriptor.getPartitioner().getRandomToken()); + ss.removeNode(joiningId.toUUID().toString()); } } diff --git a/test/unit/org/apache/cassandra/service/SerializationsTest.java b/test/unit/org/apache/cassandra/service/SerializationsTest.java index 81d310932f..b49c38b6f2 100644 --- a/test/unit/org/apache/cassandra/service/SerializationsTest.java +++ b/test/unit/org/apache/cassandra/service/SerializationsTest.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.UUID; import com.google.common.collect.Lists; + +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.io.util.FileInputStreamPlus; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -70,6 +72,7 @@ public class SerializationsTest extends AbstractSerializationsTester { DatabaseDescriptor.daemonInitialization(); partitionerSwitcher = Util.switchPartitioner(RandomPartitioner.instance); + ClusterMetadataTestHelper.setInstanceForTest(); RANDOM_UUID = TimeUUID.fromString("743325d0-4c4b-11ec-8a88-2d67081686db"); FULL_RANGE = new Range<>(Util.testPartitioner().getMinimumToken(), Util.testPartitioner().getMinimumToken()); DESC = new RepairJobDesc(RANDOM_UUID, RANDOM_UUID, "Keyspace1", "Standard1", Arrays.asList(FULL_RANGE)); diff --git a/test/unit/org/apache/cassandra/service/StorageProxyTest.java b/test/unit/org/apache/cassandra/service/StorageProxyTest.java index 6f45673390..38b8dc3182 100644 --- a/test/unit/org/apache/cassandra/service/StorageProxyTest.java +++ b/test/unit/org/apache/cassandra/service/StorageProxyTest.java @@ -19,7 +19,6 @@ package org.apache.cassandra.service; import java.net.UnknownHostException; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -32,11 +31,15 @@ import org.junit.runner.RunWith; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.gms.EndpointState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.HeartBeatState; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.StubClusterMetadataService; import org.jboss.byteman.contrib.bmunit.BMRule; import org.jboss.byteman.contrib.bmunit.BMUnitRunner; @@ -134,18 +137,13 @@ public class StorageProxyTest private void shouldHintTest(Consumer test) throws UnknownHostException { InetAddressAndPort testEp = InetAddressAndPort.getByName("192.168.1.1"); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(StubClusterMetadataService.forTesting()); + ClusterMetadataTestHelper.addEndpoint(testEp, ClusterMetadata.current().partitioner.getRandomToken()); Replica replica = full(testEp); - StorageService.instance.getTokenMetadata().updateHostId(UUID.randomUUID(), testEp); EndpointState state = new EndpointState(new HeartBeatState(0, 0)); Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.markDead(replica.endpoint(), state)); - try - { - test.accept(replica); - } - finally - { - StorageService.instance.getTokenMetadata().removeEndpoint(testEp); - } + test.accept(replica); } } diff --git a/test/unit/org/apache/cassandra/service/StorageServiceDrainTest.java b/test/unit/org/apache/cassandra/service/StorageServiceDrainTest.java index f8da0964a9..ca1e62e9ad 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceDrainTest.java +++ b/test/unit/org/apache/cassandra/service/StorageServiceDrainTest.java @@ -26,6 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; @@ -33,8 +34,6 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.RowUpdateBuilder; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionManager; -import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.utils.ByteBufferUtil; @@ -51,7 +50,7 @@ public class StorageServiceDrainTest @Before public void before() throws UnknownHostException { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); CommitLog.instance.start(); @@ -60,10 +59,7 @@ public class StorageServiceDrainTest SchemaLoader.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, TABLE)); - - StorageService.instance - .getTokenMetadata() - .updateNormalToken(new BytesToken((new byte[]{50})), InetAddressAndPort.getByName("127.0.0.1")); + StorageService.instance.unsafeSetInitialized(); final ColumnFamilyStore table = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE); for (int row = 0; row < ROWS; row++) diff --git a/test/unit/org/apache/cassandra/service/StorageServiceServerM3PTest.java b/test/unit/org/apache/cassandra/service/StorageServiceServerM3PTest.java index 8c4ab9b869..b61282fc5a 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceServerM3PTest.java +++ b/test/unit/org/apache/cassandra/service/StorageServiceServerM3PTest.java @@ -22,13 +22,10 @@ package org.apache.cassandra.service; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.util.File; -import org.apache.cassandra.locator.IEndpointSnitch; -import org.apache.cassandra.locator.PropertyFileSnitch; import static org.apache.cassandra.ServerTestUtils.cleanup; import static org.apache.cassandra.ServerTestUtils.mkdirs; @@ -42,10 +39,6 @@ public class StorageServiceServerM3PTest { GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - IEndpointSnitch snitch = new PropertyFileSnitch(); - DatabaseDescriptor.setEndpointSnitch(snitch); - Keyspace.setInitialized(); } @Test @@ -53,7 +46,8 @@ public class StorageServiceServerM3PTest { mkdirs(); cleanup(); - StorageService.instance.initServer(0); + ServerTestUtils.prepareServerNoRegister(); + StorageService.instance.initServer(); for (String path : DatabaseDescriptor.getAllDataFileLocations()) { // verify that storage directories are there. diff --git a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java index d4cf4504f8..8753413580 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java +++ b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java @@ -1,80 +1,150 @@ /* -* 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. -*/ + * 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.io.IOException; -import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; - -import org.apache.cassandra.db.SystemKeyspace; +import com.google.common.collect.Sets; import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.audit.AuditLogManager; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; +import org.apache.cassandra.dht.OrderPreservingPartitioner; import org.apache.cassandra.dht.OrderPreservingPartitioner.StringToken; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.exceptions.ConfigurationException; -import org.apache.cassandra.gms.ApplicationState; -import org.apache.cassandra.gms.Gossiper; -import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.locator.AbstractNetworkTopologySnitch; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.PropertyFileSnitch; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.schema.*; -import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.locator.WithPartitioner; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SchemaKeyspaceTables; +import org.apache.cassandra.schema.SchemaTestUtil; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.Startup; import org.assertj.core.api.Assertions; -import static org.apache.cassandra.ServerTestUtils.cleanup; -import static org.apache.cassandra.ServerTestUtils.mkdirs; import static org.apache.cassandra.config.CassandraRelevantProperties.GOSSIP_DISABLE_THREAD_VALIDATION; -import static org.apache.cassandra.config.CassandraRelevantProperties.REPLACE_ADDRESS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class StorageServiceServerTest { + static final String DC1 = "DC1"; + static final String DC2 = "DC2"; + static final String RACK = "rack1"; + + static InetAddressAndPort id1; + static InetAddressAndPort id2; + static InetAddressAndPort id3; + static InetAddressAndPort id4; + static InetAddressAndPort id5; + @BeforeClass - public static void setUp() throws ConfigurationException + public static void setUp() throws ConfigurationException, UnknownHostException { GOSSIP_DISABLE_THREAD_VALIDATION.setBoolean(true); - DatabaseDescriptor.daemonInitialization(); - CommitLog.instance.start(); - IEndpointSnitch snitch = new PropertyFileSnitch(); + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(OrderPreservingPartitioner.instance); + IEndpointSnitch snitch = new AbstractNetworkTopologySnitch() + { + @Override + public String getRack(InetAddressAndPort endpoint) + { + return location(endpoint).rack; + } + + @Override + public String getDatacenter(InetAddressAndPort endpoint) + { + return location(endpoint).datacenter; + } + + private Location location(InetAddressAndPort endpoint) + { + ClusterMetadata metadata = ClusterMetadata.current(); + NodeId id = metadata.directory.peerId(endpoint); + if (id == null) + throw new IllegalArgumentException("Unknown endpoint " + endpoint); + return metadata.directory.location(id); + } + }; + ServerTestUtils.prepareServerNoRegister(); DatabaseDescriptor.setEndpointSnitch(snitch); - Keyspace.setInitialized(); - mkdirs(); - cleanup(); - StorageService.instance.initServer(0); + + id1 = InetAddressAndPort.getByName("127.0.0.1"); + id2 = InetAddressAndPort.getByName("127.0.0.2"); + id3 = InetAddressAndPort.getByName("127.0.0.3"); + id4 = InetAddressAndPort.getByName("127.0.0.4"); + id5 = InetAddressAndPort.getByName("127.0.0.5"); + registerNodes(); + ServerTestUtils.markCMS(); + } + + private static void registerNodes() + { + ClusterMetadataTestHelper.register(id1, DC1, RACK); + ClusterMetadataTestHelper.register(id2, DC1, RACK); + ClusterMetadataTestHelper.register(id3, DC1, RACK); + + ClusterMetadataTestHelper.register(id4, DC2, RACK); + ClusterMetadataTestHelper.register(id5, DC2, RACK); + } + + private static void setupDefaultPlacements() + { + // DC1 + ClusterMetadataTestHelper.join(id1, new StringToken("A")); + ClusterMetadataTestHelper.join(id2, new StringToken("C")); + // DC2 + ClusterMetadataTestHelper.join(id4, new StringToken("B")); + ClusterMetadataTestHelper.join(id5, new StringToken("D")); + } + + @Before + public void resetCMS() + { + ServerTestUtils.resetCMS(); } @Test @@ -108,16 +178,7 @@ public class StorageServiceServerTest @Test public void testLocalPrimaryRangeForEndpointWithNetworkTopologyStrategy() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); - - // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); + setupDefaultPlacements(); Map configOptions = new HashMap<>(); configOptions.put("DC1", "2"); @@ -130,35 +191,25 @@ public class StorageServiceServerTest Collection> primaryRanges = StorageService.instance.getLocalPrimaryRangeForEndpoint(InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("A"))); primaryRanges = StorageService.instance.getLocalPrimaryRangeForEndpoint(InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("C"))); primaryRanges = StorageService.instance.getLocalPrimaryRangeForEndpoint(InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("B"))); primaryRanges = StorageService.instance.getLocalPrimaryRangeForEndpoint(InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("D"))); } @Test public void testPrimaryRangeForEndpointWithinDCWithNetworkTopologyStrategy() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); - - // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); - + setupDefaultPlacements(); Map configOptions = new HashMap<>(); configOptions.put("DC1", "1"); configOptions.put("DC2", "1"); @@ -171,37 +222,29 @@ public class StorageServiceServerTest Collection> primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("A"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("A"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); } @Test public void testPrimaryRangesWithNetworkTopologyStrategy() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); - // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); - + setupDefaultPlacements(); Map configOptions = new HashMap<>(); configOptions.put("DC1", "1"); configOptions.put("DC2", "1"); @@ -213,33 +256,25 @@ public class StorageServiceServerTest Collection> primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("A"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); } @Test public void testPrimaryRangesWithNetworkTopologyStrategyOneDCOnly() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); - // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); - + setupDefaultPlacements(); Map configOptions = new HashMap<>(); configOptions.put("DC2", "2"); configOptions.put(ReplicationParams.CLASS, "NetworkTopologyStrategy"); @@ -258,27 +293,19 @@ public class StorageServiceServerTest // endpoints in DC2 should have primary ranges which also cover DC1 primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("A"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); } @Test public void testPrimaryRangeForEndpointWithinDCWithNetworkTopologyStrategyOneDCOnly() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.2")); - // DC2 - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.4")); - metadata.updateNormalToken(new StringToken("D"), InetAddressAndPort.getByName("127.0.0.5")); - + setupDefaultPlacements(); Map configOptions = new HashMap<>(); configOptions.put("DC2", "2"); configOptions.put(ReplicationParams.CLASS, "NetworkTopologyStrategy"); @@ -288,48 +315,33 @@ public class StorageServiceServerTest SchemaTestUtil.addOrUpdateKeyspace(meta, false); // endpoints in DC1 should not have primary range - Collection> primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.1")); + Collection> primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, id1); Assertions.assertThat(primaryRanges).isEmpty(); - primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, - InetAddressAndPort.getByName("127.0.0.2")); + primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, id2); Assertions.assertThat(primaryRanges).isEmpty(); // endpoints in DC2 should have primary ranges which also cover DC1 - primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.4")); + primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, id4); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("A"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); - primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.5")); + primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, id5); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(2); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); } @Test public void testPrimaryRangesWithVnodes() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); // DC1 - Multimap dc1 = HashMultimap.create(); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("A")); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("E")); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("H")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("C")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("I")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("J")); - metadata.updateNormalTokens(dc1); + ClusterMetadataTestHelper.join(id1, Sets.newHashSet(new StringToken("A"), new StringToken("E"), new StringToken("H"))); + ClusterMetadataTestHelper.join(id2, Sets.newHashSet(new StringToken("C"), new StringToken("I"), new StringToken("J"))); // DC2 - Multimap dc2 = HashMultimap.create(); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("B")); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("G")); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("L")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("D")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("F")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("K")); - metadata.updateNormalTokens(dc2); + ClusterMetadataTestHelper.join(id4, Sets.newHashSet(new StringToken("B"), new StringToken("G"), new StringToken("L"))); + ClusterMetadataTestHelper.join(id5, Sets.newHashSet(new StringToken("D"), new StringToken("F"), new StringToken("K"))); Map configOptions = new HashMap<>(); configOptions.put("DC2", "2"); @@ -349,53 +361,37 @@ public class StorageServiceServerTest // endpoints in DC2 should have primary ranges which also cover DC1 primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(4); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("F"), new StringToken("G"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("K"), new StringToken("L"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("F"), new StringToken("G"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("K"), new StringToken("L"))); // because /127.0.0.4 holds token "B" which is the next to token "A" from /127.0.0.1, // the node covers range (L, A] - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("L"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("L"), new StringToken("A"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(8); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("E"), new StringToken("F"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("J"), new StringToken("K"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("E"), new StringToken("F"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("J"), new StringToken("K"))); // ranges from /127.0.0.1 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("E"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("E"))); // the next token to "H" in DC2 is "K" in /127.0.0.5, so (G, H] goes to /127.0.0.5 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("G"), new StringToken("H"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("G"), new StringToken("H"))); // ranges from /127.0.0.2 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("H"), new StringToken("I"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("I"), new StringToken("J"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("H"), new StringToken("I"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("I"), new StringToken("J"))); } @Test public void testPrimaryRangeForEndpointWithinDCWithVnodes() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - // DC1 - Multimap dc1 = HashMultimap.create(); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("A")); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("E")); - dc1.put(InetAddressAndPort.getByName("127.0.0.1"), new StringToken("H")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("C")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("I")); - dc1.put(InetAddressAndPort.getByName("127.0.0.2"), new StringToken("J")); - metadata.updateNormalTokens(dc1); - + ClusterMetadataTestHelper.join(id1, Sets.newHashSet(new StringToken("A"), new StringToken("E"), new StringToken("H"))); + ClusterMetadataTestHelper.join(id2, Sets.newHashSet(new StringToken("C"), new StringToken("I"), new StringToken("J"))); // DC2 - Multimap dc2 = HashMultimap.create(); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("B")); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("G")); - dc2.put(InetAddressAndPort.getByName("127.0.0.4"), new StringToken("L")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("D")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("F")); - dc2.put(InetAddressAndPort.getByName("127.0.0.5"), new StringToken("K")); - metadata.updateNormalTokens(dc2); + ClusterMetadataTestHelper.join(id4, Sets.newHashSet(new StringToken("B"), new StringToken("G"), new StringToken("L"))); + ClusterMetadataTestHelper.join(id5, Sets.newHashSet(new StringToken("D"), new StringToken("F"), new StringToken("K"))); Map configOptions = new HashMap<>(); configOptions.put("DC1", "1"); @@ -409,57 +405,54 @@ public class StorageServiceServerTest // endpoints in DC1 should have primary ranges which also cover DC2 Collection> primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(8); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("J"), new StringToken("K"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("K"), new StringToken("L"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("L"), new StringToken("A"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("E"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("E"), new StringToken("F"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("F"), new StringToken("G"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("G"), new StringToken("H"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("J"), new StringToken("K"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("K"), new StringToken("L"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("L"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("E"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("E"), new StringToken("F"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("F"), new StringToken("G"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("G"), new StringToken("H"))); // endpoints in DC1 should have primary ranges which also cover DC2 primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(4); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("H"), new StringToken("I"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("I"), new StringToken("J"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("H"), new StringToken("I"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("I"), new StringToken("J"))); // endpoints in DC2 should have primary ranges which also cover DC1 primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.4")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(4); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("F"), new StringToken("G"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("K"), new StringToken("L"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("F"), new StringToken("G"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("K"), new StringToken("L"))); // because /127.0.0.4 holds token "B" which is the next to token "A" from /127.0.0.1, // the node covers range (L, A] - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("L"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("L"), new StringToken("A"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.5")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(8); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("D"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("E"), new StringToken("F"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("J"), new StringToken("K"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("D"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("E"), new StringToken("F"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("J"), new StringToken("K"))); // ranges from /127.0.0.1 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("D"), new StringToken("E"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("D"), new StringToken("E"))); // the next token to "H" in DC2 is "K" in /127.0.0.5, so (G, H] goes to /127.0.0.5 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("G"), new StringToken("H"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("G"), new StringToken("H"))); // ranges from /127.0.0.2 - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("H"), new StringToken("I"))); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("I"), new StringToken("J"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("H"), new StringToken("I"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("I"), new StringToken("J"))); } @Test public void testPrimaryRangesWithSimpleStrategy() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.2")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.3")); + ClusterMetadataTestHelper.join(id1, new StringToken("A")); + ClusterMetadataTestHelper.join(id2, new StringToken("B")); + ClusterMetadataTestHelper.join(id3, new StringToken("C")); SchemaTestUtil.dropKeyspaceIfExist("Keyspace1", false); KeyspaceMetadata meta = KeyspaceMetadata.create("Keyspace1", KeyspaceParams.simpleTransient(2)); @@ -467,27 +460,24 @@ public class StorageServiceServerTest Collection> primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("A"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); primaryRanges = StorageService.instance.getPrimaryRangesForEndpoint(meta.name, InetAddressAndPort.getByName("127.0.0.3")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); } /* Does not make much sense to use -local and -pr with simplestrategy, but just to prevent human errors */ @Test public void testPrimaryRangeForEndpointWithinDCWithSimpleStrategy() throws Exception { - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - - metadata.updateNormalToken(new StringToken("A"), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new StringToken("B"), InetAddressAndPort.getByName("127.0.0.2")); - metadata.updateNormalToken(new StringToken("C"), InetAddressAndPort.getByName("127.0.0.3")); + ClusterMetadataTestHelper.join(id1, new StringToken("A")); + ClusterMetadataTestHelper.join(id2, new StringToken("B")); + ClusterMetadataTestHelper.join(id3, new StringToken("C")); Map configOptions = new HashMap<>(); configOptions.put("replication_factor", "2"); @@ -498,81 +488,77 @@ public class StorageServiceServerTest Collection> primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.1")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("C"), new StringToken("A"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("C"), new StringToken("A"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.2")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("A"), new StringToken("B"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("A"), new StringToken("B"))); primaryRanges = StorageService.instance.getPrimaryRangeForEndpointWithinDC(meta.name, InetAddressAndPort.getByName("127.0.0.3")); Assertions.assertThat(primaryRanges.size()).as(primaryRanges.toString()).isEqualTo(1); - Assertions.assertThat(primaryRanges).contains(new Range(new StringToken("B"), new StringToken("C"))); + Assertions.assertThat(primaryRanges).contains(new Range<>(new StringToken("B"), new StringToken("C"))); } @Test public void testCreateRepairRangeFrom() throws Exception { - StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + try (WithPartitioner m3p = new WithPartitioner(Murmur3Partitioner.instance)) + { + registerNodes(); + ClusterMetadataTestHelper.join(id1, new LongToken(1000L)); + ClusterMetadataTestHelper.join(id2, new LongToken(2000L)); + ClusterMetadataTestHelper.join(id3, new LongToken(3000L)); + ClusterMetadataTestHelper.join(id4, new LongToken(4000L)); - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); + Collection> repairRangeFrom = StorageService.instance.createRepairRangeFrom("1500", "3700"); + Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(3); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(1500L), new LongToken(2000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(2000L), new LongToken(3000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(3000L), new LongToken(3700L))); - metadata.updateNormalToken(new LongToken(1000L), InetAddressAndPort.getByName("127.0.0.1")); - metadata.updateNormalToken(new LongToken(2000L), InetAddressAndPort.getByName("127.0.0.2")); - metadata.updateNormalToken(new LongToken(3000L), InetAddressAndPort.getByName("127.0.0.3")); - metadata.updateNormalToken(new LongToken(4000L), InetAddressAndPort.getByName("127.0.0.4")); + repairRangeFrom = StorageService.instance.createRepairRangeFrom("500", "700"); + Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(1); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(500L), new LongToken(700L))); - Collection> repairRangeFrom = StorageService.instance.createRepairRangeFrom("1500", "3700"); - Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(3); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(1500L), new LongToken(2000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(2000L), new LongToken(3000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(3000L), new LongToken(3700L))); + repairRangeFrom = StorageService.instance.createRepairRangeFrom("500", "1700"); + Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(2); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(500L), new LongToken(1000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(1000L), new LongToken(1700L))); - repairRangeFrom = StorageService.instance.createRepairRangeFrom("500", "700"); - Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(1); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(500L), new LongToken(700L))); + repairRangeFrom = StorageService.instance.createRepairRangeFrom("2500", "2300"); + Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(5); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(2500L), new LongToken(3000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(3000L), new LongToken(4000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(4000L), new LongToken(1000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(1000L), new LongToken(2000L))); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(2000L), new LongToken(2300L))); - repairRangeFrom = StorageService.instance.createRepairRangeFrom("500", "1700"); - Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(2); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(500L), new LongToken(1000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(1000L), new LongToken(1700L))); + repairRangeFrom = StorageService.instance.createRepairRangeFrom("2000", "3000"); + Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(1); + Assertions.assertThat(repairRangeFrom).contains(new Range<>(new LongToken(2000L), new LongToken(3000L))); - repairRangeFrom = StorageService.instance.createRepairRangeFrom("2500", "2300"); - Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(5); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(2500L), new LongToken(3000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(3000L), new LongToken(4000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(4000L), new LongToken(1000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(1000L), new LongToken(2000L))); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(2000L), new LongToken(2300L))); - - repairRangeFrom = StorageService.instance.createRepairRangeFrom("2000", "3000"); - Assertions.assertThat(repairRangeFrom.size()).as(repairRangeFrom.toString()).isEqualTo(1); - Assertions.assertThat(repairRangeFrom).contains(new Range(new LongToken(2000L), new LongToken(3000L))); - - repairRangeFrom = StorageService.instance.createRepairRangeFrom("2000", "2000"); - Assertions.assertThat(repairRangeFrom).isEmpty(); + repairRangeFrom = StorageService.instance.createRepairRangeFrom("2000", "2000"); + Assertions.assertThat(repairRangeFrom).isEmpty(); + } } /** - * Test that StorageService.getNativeAddress returns the correct value based on available yaml and gossip state + * Test that StorageService.getNativeAddress returns the correct value based on cluster metadata + * * @throws Exception */ @Test public void testGetNativeAddress() throws Exception { - String internalAddressString = "127.0.0.2:666"; - InetAddressAndPort internalAddress = InetAddressAndPort.getByName(internalAddressString); - Gossiper.instance.addSavedEndpoint(internalAddress); - //Default to using the provided address with the configured port - assertEquals("127.0.0.2:" + DatabaseDescriptor.getNativeTransportPort(), StorageService.instance.getNativeaddress(internalAddress, true)); + NodeId node2 = ClusterMetadata.current().directory.peerId(id2); + NodeAddresses oldAddresses = ClusterMetadata.current().directory.getNodeAddresses(node2); + assertEquals("127.0.0.2:7012", StorageService.instance.getNativeaddress(id2, true)); - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(Murmur3Partitioner.instance); - //If we don't have the port use the gossip address, but with the configured port - Gossiper.instance.getEndpointStateForEndpoint(internalAddress).addApplicationState(ApplicationState.RPC_ADDRESS, valueFactory.rpcaddress(InetAddress.getByName("127.0.0.3"))); - assertEquals("127.0.0.3:" + DatabaseDescriptor.getNativeTransportPort(), StorageService.instance.getNativeaddress(internalAddress, true)); - //If we have the address and port in gossip use that - Gossiper.instance.getEndpointStateForEndpoint(internalAddress).addApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT, valueFactory.nativeaddressAndPort(InetAddressAndPort.getByName("127.0.0.3:666"))); - assertEquals("127.0.0.3:666", StorageService.instance.getNativeaddress(internalAddress, true)); + String newNativeString = "127.1.1.2:19012"; + InetAddressAndPort newNativeAddress = InetAddressAndPort.getByName(newNativeString); + NodeAddresses newAddresses = new NodeAddresses(UUID.randomUUID(), oldAddresses.broadcastAddress, oldAddresses.localAddress, newNativeAddress); + ClusterMetadataService.instance().commit(new Startup(node2, newAddresses, NodeVersion.CURRENT)); + assertEquals(newNativeString, StorageService.instance.getNativeaddress(id2, true)); } @Test @@ -580,21 +566,17 @@ public class StorageServiceServerTest { // Ensure IPv6 addresses are properly bracketed in RFC2732 (https://datatracker.ietf.org/doc/html/rfc2732) format when including ports. // See https://issues.apache.org/jira/browse/CASSANDRA-17945 for more context. - String internalAddressIPV6String = "[0:0:0:0:0:0:0:3]:666"; - InetAddressAndPort internalAddressIPV6 = InetAddressAndPort.getByName(internalAddressIPV6String); - Gossiper.instance.addSavedEndpoint(internalAddressIPV6); + NodeId node2 = ClusterMetadata.current().directory.peerId(id2); + NodeAddresses oldAddresses = ClusterMetadata.current().directory.getNodeAddresses(node2); + assertEquals("127.0.0.2:7012", StorageService.instance.getNativeaddress(id2, true)); + String newNativeString = "[0:0:0:0:0:0:0:3]:666"; + InetAddressAndPort newNativeAddress = InetAddressAndPort.getByName(newNativeString); + NodeAddresses newAddresses = new NodeAddresses(UUID.randomUUID(), oldAddresses.broadcastAddress, oldAddresses.localAddress, newNativeAddress); + ClusterMetadataService.instance().commit(new Startup(node2, newAddresses, NodeVersion.CURRENT)); + assertEquals(newNativeString, StorageService.instance.getNativeaddress(id2, true)); //Default to using the provided address with the configured port - assertEquals("[0:0:0:0:0:0:0:3]:" + DatabaseDescriptor.getNativeTransportPort(), StorageService.instance.getNativeaddress(internalAddressIPV6, true)); - - VersionedValue.VersionedValueFactory valueFactory = new VersionedValue.VersionedValueFactory(Murmur3Partitioner.instance); - //If RPC_ADDRESS is present with an IPv6 address, we should properly bracket encode the IP with the configured port. - Gossiper.instance.getEndpointStateForEndpoint(internalAddressIPV6).addApplicationState(ApplicationState.RPC_ADDRESS, valueFactory.rpcaddress(InetAddress.getByName("0:0:0:0:0:0:5a:3"))); - assertEquals("[0:0:0:0:0:0:5a:3]:" + DatabaseDescriptor.getNativeTransportPort(), StorageService.instance.getNativeaddress(internalAddressIPV6, true)); - - //If we have the address and port in gossip use that - Gossiper.instance.getEndpointStateForEndpoint(internalAddressIPV6).addApplicationState(ApplicationState.NATIVE_ADDRESS_AND_PORT, valueFactory.nativeaddressAndPort(InetAddressAndPort.getByName("[0:0:0:0:0:0:5c:3]:8675"))); - assertEquals("[0:0:0:0:0:0:5c:3]:8675", StorageService.instance.getNativeaddress(internalAddressIPV6, true)); + assertEquals(newNativeString, StorageService.instance.getNativeaddress(id2, true)); } @Test @@ -632,30 +614,4 @@ public class StorageServiceServerTest assertTrue(AuditLogManager.instance.isEnabled()); StorageService.instance.disableAuditLog(); } - - @Test - public void isReplacingSameHostAddressAndHostIdTest() throws UnknownHostException - { - try (WithProperties properties = new WithProperties()) - { - UUID differentHostId = UUID.randomUUID(); - Assert.assertFalse(StorageService.instance.isReplacingSameHostAddressAndHostId(differentHostId)); - - final String hostAddress = FBUtilities.getBroadcastAddressAndPort().getHostAddress(false); - UUID localHostId = SystemKeyspace.getOrInitializeLocalHostId(); - Gossiper.instance.initializeNodeUnsafe(FBUtilities.getBroadcastAddressAndPort(), localHostId, 1); - - // Check detects replacing the same host address with the same hostid - REPLACE_ADDRESS.setString(hostAddress); - Assert.assertTrue(StorageService.instance.isReplacingSameHostAddressAndHostId(localHostId)); - - // Check detects replacing the same host address with a different host id - REPLACE_ADDRESS.setString(hostAddress); - Assert.assertFalse(StorageService.instance.isReplacingSameHostAddressAndHostId(differentHostId)); - - // Check tolerates the DNS entry going away for the replace_address - REPLACE_ADDRESS.setString("unresolvable.host.local."); - Assert.assertFalse(StorageService.instance.isReplacingSameHostAddressAndHostId(differentHostId)); - } - } -} \ No newline at end of file +} diff --git a/test/unit/org/apache/cassandra/service/StorageServiceTest.java b/test/unit/org/apache/cassandra/service/StorageServiceTest.java index e070095910..725f105db8 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceTest.java +++ b/test/unit/org/apache/cassandra/service/StorageServiceTest.java @@ -21,30 +21,27 @@ package org.apache.cassandra.service; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; -import com.google.common.collect.ImmutableMultimap; -import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.commitlog.CommitLog; -import org.apache.cassandra.dht.RandomPartitioner; -import org.apache.cassandra.dht.Range; -import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.locator.AbstractEndpointSnitch; -import org.apache.cassandra.locator.AbstractReplicationStrategy; -import org.apache.cassandra.locator.EndpointsByReplica; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; import org.apache.cassandra.locator.ReplicaMultimap; import org.apache.cassandra.locator.SimpleSnitch; -import org.apache.cassandra.locator.SimpleStrategy; -import org.apache.cassandra.locator.TokenMetadata; -import org.mockito.Mockito; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.Register; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; @@ -54,7 +51,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -public class StorageServiceTest +public class StorageServiceTest extends TestBaseImpl { static InetAddressAndPort aAddress; static InetAddressAndPort bAddress; @@ -70,25 +67,14 @@ public class StorageServiceTest cAddress = InetAddressAndPort.getByName("127.0.0.3"); dAddress = InetAddressAndPort.getByName("127.0.0.4"); eAddress = InetAddressAndPort.getByName("127.0.0.5"); - } - private static final Token threeToken = new RandomPartitioner.BigIntegerToken("3"); - private static final Token sixToken = new RandomPartitioner.BigIntegerToken("6"); - private static final Token nineToken = new RandomPartitioner.BigIntegerToken("9"); - private static final Token elevenToken = new RandomPartitioner.BigIntegerToken("11"); - private static final Token oneToken = new RandomPartitioner.BigIntegerToken("1"); - - Range aRange = new Range<>(oneToken, threeToken); - Range bRange = new Range<>(threeToken, sixToken); - Range cRange = new Range<>(sixToken, nineToken); - Range dRange = new Range<>(nineToken, elevenToken); - Range eRange = new Range<>(elevenToken, oneToken); - - @Before - public void setUp() - { + ServerTestUtils.prepareServerNoRegister(); DatabaseDescriptor.daemonInitialization(); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + + ClusterMetadataService.instance().commit(new Register(NodeAddresses.current(), + new Location(SimpleSnitch.DATA_CENTER_NAME, SimpleSnitch.RACK_NAME), + NodeVersion.CURRENT)); IEndpointSnitch snitch = new AbstractEndpointSnitch() { public int compareEndpoints(InetAddressAndPort target, Replica r1, Replica r2) @@ -111,12 +97,10 @@ public class StorageServiceTest CommitLog.instance.start(); } - private AbstractReplicationStrategy simpleStrategy(TokenMetadata tmd) + @Before + public void setUp() { - return new SimpleStrategy("MoveTransientTest", - tmd, - DatabaseDescriptor.getEndpointSnitch(), - com.google.common.collect.ImmutableMap.of("replication_factor", "3/1")); + Rebuild.unsafeResetRebuilding(); } public static > void assertMultimapEqualsIgnoreOrder(ReplicaMultimap a, ReplicaMultimap b) @@ -145,39 +129,14 @@ public class StorageServiceTest public static String formatClassAndValue(Object value) { String className = value == null ? "null" : value.getClass().getName(); - return className + "<" + String.valueOf(value) + ">"; - } - - @Test - public void testGetChangedReplicasForLeaving() throws Exception - { - TokenMetadata tmd = new TokenMetadata(); - tmd.updateNormalToken(threeToken, aAddress); - tmd.updateNormalToken(sixToken, bAddress); - tmd.updateNormalToken(nineToken, cAddress); - tmd.updateNormalToken(elevenToken, dAddress); - tmd.updateNormalToken(oneToken, eAddress); - - tmd.addLeavingEndpoint(aAddress); - - AbstractReplicationStrategy strat = simpleStrategy(tmd); - - EndpointsByReplica result = StorageService.getChangedReplicasForLeaving("StorageServiceTest", aAddress, tmd, strat); - System.out.println(result); - EndpointsByReplica.Builder expectedResult = new EndpointsByReplica.Builder(); - expectedResult.put(new Replica(aAddress, aRange, true), new Replica(cAddress, new Range<>(oneToken, sixToken), true)); - expectedResult.put(new Replica(aAddress, aRange, true), new Replica(dAddress, new Range<>(oneToken, sixToken), false)); - expectedResult.put(new Replica(aAddress, eRange, true), new Replica(bAddress, eRange, true)); - expectedResult.put(new Replica(aAddress, eRange, true), new Replica(cAddress, eRange, false)); - expectedResult.put(new Replica(aAddress, dRange, false), new Replica(bAddress, dRange, false)); - assertMultimapEqualsIgnoreOrder(result, expectedResult.build()); + return String.format("%s<%s>", className, value); } @Test public void testSetGetSSTablePreemptiveOpenIntervalInMB() { StorageService.instance.setSSTablePreemptiveOpenIntervalInMB(-1); - Assert.assertEquals(-1, StorageService.instance.getSSTablePreemptiveOpenIntervalInMB()); + assertEquals(-1, StorageService.instance.getSSTablePreemptiveOpenIntervalInMB()); } @Test @@ -210,9 +169,9 @@ public class StorageServiceTest int previousDepth = storageService.getRepairSessionMaximumTreeDepth(); try { - Assert.assertEquals(20, storageService.getRepairSessionMaximumTreeDepth()); + assertEquals(20, storageService.getRepairSessionMaximumTreeDepth()); storageService.setRepairSessionMaximumTreeDepth(10); - Assert.assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); + assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); try { @@ -220,7 +179,7 @@ public class StorageServiceTest fail("Should have received a IllegalArgumentException for depth of 9"); } catch (IllegalArgumentException ignored) { } - Assert.assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); + assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); try { @@ -228,10 +187,10 @@ public class StorageServiceTest fail("Should have received a IllegalArgumentException for depth of -20"); } catch (IllegalArgumentException ignored) { } - Assert.assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); + assertEquals(10, storageService.getRepairSessionMaximumTreeDepth()); storageService.setRepairSessionMaximumTreeDepth(22); - Assert.assertEquals(22, storageService.getRepairSessionMaximumTreeDepth()); + assertEquals(22, storageService.getRepairSessionMaximumTreeDepth()); } finally { @@ -247,7 +206,7 @@ public class StorageServiceTest try { storageService.setColumnIndexSizeInKiB(1024); - Assert.assertEquals(1024, storageService.getColumnIndexSizeInKiB()); + assertEquals(1024, storageService.getColumnIndexSizeInKiB()); try { @@ -255,7 +214,7 @@ public class StorageServiceTest fail("Should have received an IllegalArgumentException column_index_size = 2GiB"); } catch (IllegalArgumentException ignored) { } - Assert.assertEquals(1024, storageService.getColumnIndexSizeInKiB()); + assertEquals(1024, storageService.getColumnIndexSizeInKiB()); } finally { @@ -271,7 +230,7 @@ public class StorageServiceTest try { storageService.setColumnIndexCacheSizeInKiB(1024); - Assert.assertEquals(1024, storageService.getColumnIndexCacheSizeInKiB()); + assertEquals(1024, storageService.getColumnIndexCacheSizeInKiB()); try { @@ -279,7 +238,7 @@ public class StorageServiceTest fail("Should have received an IllegalArgumentException column_index_cache_size= 2GiB"); } catch (IllegalArgumentException ignored) { } - Assert.assertEquals(1024, storageService.getColumnIndexCacheSizeInKiB()); + assertEquals(1024, storageService.getColumnIndexCacheSizeInKiB()); } finally { @@ -295,7 +254,7 @@ public class StorageServiceTest try { storageService.setBatchSizeWarnThresholdInKiB(1024); - Assert.assertEquals(1024, storageService.getBatchSizeWarnThresholdInKiB()); + assertEquals(1024, storageService.getBatchSizeWarnThresholdInKiB()); try { @@ -303,7 +262,7 @@ public class StorageServiceTest fail("Should have received an IllegalArgumentException batch_size_warn_threshold = 2GiB"); } catch (IllegalArgumentException ignored) { } - Assert.assertEquals(1024, storageService.getBatchSizeWarnThresholdInKiB()); + assertEquals(1024, storageService.getBatchSizeWarnThresholdInKiB()); } finally { @@ -316,12 +275,12 @@ public class StorageServiceTest { try { - getStorageService().rebuild(DatabaseDescriptor.getLocalDataCenter(), "StorageServiceTest", null, null, true); + StorageService.instance.rebuild(DatabaseDescriptor.getLocalDataCenter(), "StorageServiceTest", null, null, true); fail(); } catch (IllegalArgumentException e) { - Assert.assertEquals("Cannot set source data center to be local data center, when excludeLocalDataCenter flag is set", e.getMessage()); + assertEquals("Cannot set source data center to be local data center, when excludeLocalDataCenter flag is set", e.getMessage()); } } @@ -332,14 +291,14 @@ public class StorageServiceTest try { - getStorageService().rebuild(nonExistentDC, "StorageServiceTest", null, null, true); + StorageService.instance.rebuild(nonExistentDC, "StorageServiceTest", null, null, true); fail(); } catch (IllegalArgumentException ex) { - Assert.assertEquals(String.format("Provided datacenter '%s' is not a valid datacenter, available datacenters are: %s", - nonExistentDC, - SimpleSnitch.DATA_CENTER_NAME), + assertEquals(String.format("Provided datacenter '%s' is not a valid datacenter, available datacenters are: %s", + nonExistentDC, + SimpleSnitch.DATA_CENTER_NAME), ex.getMessage()); } } @@ -349,7 +308,7 @@ public class StorageServiceTest { try { - getStorageService().rebuild("datacenter1", null, "123", null); + StorageService.instance.rebuild("datacenter1", null, "123", null); fail(); } catch (IllegalArgumentException ex) @@ -357,24 +316,4 @@ public class StorageServiceTest assertEquals("Cannot specify tokens without keyspace.", ex.getMessage()); } } - - private StorageService getStorageService() - { - ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - builder.put(SimpleSnitch.DATA_CENTER_NAME, aAddress); - - TokenMetadata.Topology tokenMetadataTopology = Mockito.mock(TokenMetadata.Topology.class); - Mockito.when(tokenMetadataTopology.getDatacenterEndpoints()).thenReturn(builder.build()); - - TokenMetadata metadata = new TokenMetadata(new SimpleSnitch()); - TokenMetadata spiedMetadata = Mockito.spy(metadata); - - Mockito.when(spiedMetadata.getTopology()).thenReturn(tokenMetadataTopology); - - StorageService spiedStorageService = Mockito.spy(StorageService.instance); - Mockito.when(spiedStorageService.getTokenMetadata()).thenReturn(spiedMetadata); - Mockito.when(spiedMetadata.cloneOnlyTokenMap()).thenReturn(spiedMetadata); - - return spiedStorageService; - } } diff --git a/test/unit/org/apache/cassandra/service/WriteResponseHandlerTest.java b/test/unit/org/apache/cassandra/service/WriteResponseHandlerTest.java index 7a9bbf33c8..f913714e7a 100644 --- a/test/unit/org/apache/cassandra/service/WriteResponseHandlerTest.java +++ b/test/unit/org/apache/cassandra/service/WriteResponseHandlerTest.java @@ -21,13 +21,9 @@ package org.apache.cassandra.service; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.UUID; import java.util.concurrent.TimeUnit; import com.google.common.base.Predicates; -import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.ReplicaPlans; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -38,15 +34,18 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; +import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.locator.ReplicaUtils; -import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.ByteBufferUtil; import static java.util.concurrent.TimeUnit.DAYS; @@ -80,10 +79,9 @@ public class WriteResponseHandlerTest SchemaLoader.loadSchema(); DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); // Register peers with expected DC for NetworkTopologyStrategy. - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.255")); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.2.0.255")); +// metadata.clearUnsafe(); +// metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.255")); +// metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.2.0.255")); DatabaseDescriptor.setEndpointSnitch(new IEndpointSnitch() { @@ -267,7 +265,7 @@ public class WriteResponseHandlerTest private static AbstractWriteResponseHandler createWriteResponseHandler(ConsistencyLevel cl, ConsistencyLevel ideal, long queryStartTime) { - return ks.getReplicationStrategy().getWriteResponseHandler(ReplicaPlans.forWrite(ks, cl, targets, pending, Predicates.alwaysTrue(), ReplicaPlans.writeAll), + return ks.getReplicationStrategy().getWriteResponseHandler(ReplicaPlans.forWrite(ks, cl, (cm) -> targets, (cm) -> pending, Epoch.FIRST, Predicates.alwaysTrue(), ReplicaPlans.writeAll), null, WriteType.SIMPLE, null, queryStartTime, ideal); } diff --git a/test/unit/org/apache/cassandra/service/WriteResponseHandlerTransientTest.java b/test/unit/org/apache/cassandra/service/WriteResponseHandlerTransientTest.java index f912c722be..7eef9d7def 100644 --- a/test/unit/org/apache/cassandra/service/WriteResponseHandlerTransientTest.java +++ b/test/unit/org/apache/cassandra/service/WriteResponseHandlerTransientTest.java @@ -21,19 +21,10 @@ package org.apache.cassandra.service; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Set; -import java.util.UUID; import java.util.function.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; - -import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.locator.EndpointsForToken; -import org.apache.cassandra.locator.ReplicaLayout; -import org.apache.cassandra.locator.EndpointsForRange; -import org.apache.cassandra.locator.ReplicaPlan; -import org.apache.cassandra.locator.ReplicaPlans; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -43,13 +34,20 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaCollection; -import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.locator.ReplicaLayout; +import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.locator.ReplicaUtils.full; @@ -95,10 +93,10 @@ public class WriteResponseHandlerTransientTest DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); // Register peers with expected DC for NetworkTopologyStrategy. - TokenMetadata metadata = StorageService.instance.getTokenMetadata(); - metadata.clearUnsafe(); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.1")); - metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.2.0.1")); + +// metadata.clearUnsafe(); +// metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.1.0.1")); +// metadata.updateHostId(UUID.randomUUID(), InetAddressAndPort.getByName("127.2.0.1")); DatabaseDescriptor.setEndpointSnitch(new IEndpointSnitch() { @@ -150,7 +148,7 @@ public class WriteResponseHandlerTransientTest EndpointsForToken natural = EndpointsForToken.of(dummy.getToken(), full(EP1), full(EP2), trans(EP3), full(EP5)); EndpointsForToken pending = EndpointsForToken.of(dummy.getToken(), full(EP4), trans(EP6)); ReplicaLayout.ForTokenWrite layout = new ReplicaLayout.ForTokenWrite(ks.getReplicationStrategy(), natural, pending); - ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(ks, ConsistencyLevel.QUORUM, layout, layout, ReplicaPlans.writeAll); + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(ks, ConsistencyLevel.QUORUM, (cm) -> layout, (r) -> true, ReplicaPlans.writeAll); Assert.assertTrue(Iterables.elementsEqual(EndpointsForRange.of(full(EP4), trans(EP6)), replicaPlan.pending())); @@ -158,14 +156,13 @@ public class WriteResponseHandlerTransientTest private static ReplicaPlan.ForWrite expected(EndpointsForToken natural, EndpointsForToken selected) { - return new ReplicaPlan.ForWrite(ks, ks.getReplicationStrategy(), ConsistencyLevel.QUORUM, EndpointsForToken.empty(dummy.getToken()), natural, natural, selected); + return new ReplicaPlan.ForWrite(ks, ks.getReplicationStrategy(), ConsistencyLevel.QUORUM, EndpointsForToken.empty(dummy.getToken()), natural, natural, selected, (cm) -> null, Epoch.EMPTY); } private static ReplicaPlan.ForWrite getSpeculationContext(EndpointsForToken natural, Predicate livePredicate) { ReplicaLayout.ForTokenWrite liveAndDown = new ReplicaLayout.ForTokenWrite(ks.getReplicationStrategy(), natural, EndpointsForToken.empty(dummy.getToken())); - ReplicaLayout.ForTokenWrite live = new ReplicaLayout.ForTokenWrite(ks.getReplicationStrategy(), natural.filter(r -> livePredicate.test(r.endpoint())), EndpointsForToken.empty(dummy.getToken())); - return ReplicaPlans.forWrite(ks, ConsistencyLevel.QUORUM, liveAndDown, live, ReplicaPlans.writeNormal); + return ReplicaPlans.forWrite(ks, ConsistencyLevel.QUORUM, (cm) -> liveAndDown, r -> livePredicate.test(r.endpoint()), ReplicaPlans.writeNormal); } private static void assertSpeculationReplicas(ReplicaPlan.ForWrite expected, EndpointsForToken replicas, Predicate livePredicate) diff --git a/test/unit/org/apache/cassandra/service/paxos/PaxosVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/service/paxos/PaxosVerbHandlerOutOfRangeTest.java new file mode 100644 index 0000000000..1ed2bef52e --- /dev/null +++ b/test/unit/org/apache/cassandra/service/paxos/PaxosVerbHandlerOutOfRangeTest.java @@ -0,0 +1,195 @@ +/* + * 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.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.service.paxos.v1.AbstractPaxosVerbHandler; +import org.apache.cassandra.service.paxos.v1.PrepareVerbHandler; +import org.apache.cassandra.service.paxos.v1.ProposeVerbHandler; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.utils.ByteBufferUtil; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.junit.Assert.assertEquals; + +public class PaxosVerbHandlerOutOfRangeTest // PaxosV1 out of range tests - V2 implements OOTR checks at the protocol level +{ + // For the purposes of this testing, the details of the Commit don't really matter + // as we're just testing the rejection (or lack of) and not the result of doing + // whatever the specific verb handlers are supposed to do when they don't reject + // a given Commit + + private static final String TEST_NAME = "paxos_vh_test_"; + private static final String KEYSPACE = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + + private long startingTotalMetricCount; + private long startingKeyspaceMetricCount; + + @BeforeClass + public static void init() throws Exception + { + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.schemaDefinition(TEST_NAME); + ServerTestUtils.markCMS(); + StorageService.instance.unsafeSetInitialized(); + } + + @Before + public void setup() throws Exception + { + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(broadcastAddress, bytesToken(100)); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + + MessagingService.instance().inboundSink.clear(); + MessagingService.instance().outboundSink.clear(); + startingTotalMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + startingKeyspaceMetricCount = keyspaceMetricValue(); + } + + private static DecoratedKey key(TableMetadata metadata, int key) + { + return metadata.partitioner.decorateKey(ByteBufferUtil.bytes(key)); + } + + @Test + public void acceptPrepareForNaturalEndpoint() throws Exception + { + acceptRequestForNaturalEndpoint(Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, new PrepareVerbHandler()); + } + + @Test + public void acceptProposeForNaturalEndpoint() throws Exception + { + acceptRequestForNaturalEndpoint(Verb.PAXOS_PROPOSE_REQ, Verb.PAXOS_PROPOSE_RSP, new ProposeVerbHandler()); + } + + private void acceptRequestForNaturalEndpoint(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception + { + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int key = 50; + Commit commit = commit(key); + handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, responseVerb, messageId, false); + } + + @Test + public void acceptPrepareForPendingEndpoint() throws Exception + { + acceptRequestForPendingEndpoint(Verb.PAXOS_PREPARE_REQ, Verb.PAXOS_PREPARE_RSP, new PrepareVerbHandler()); + } + + @Test + public void acceptProposeForPendingEndpoint() throws Exception + { + acceptRequestForPendingEndpoint(Verb.PAXOS_PROPOSE_REQ, Verb.PAXOS_PROPOSE_RSP, new ProposeVerbHandler()); + } + + private void acceptRequestForPendingEndpoint(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception + { + // reset ClusterMetadata then join the remote node and partially + // join the localhost one to emulate pending ranges + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.addEndpoint(node1, bytesToken(0)); + ClusterMetadataTestHelper.register(broadcastAddress); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, bytesToken(100)); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(broadcastAddress)); + + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int key = 50; + Commit commit = commit(key); + handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, responseVerb, messageId, false); + } + + @Test + public void rejectPrepareForTokenOutOfRange() throws Exception + { + rejectRequestForTokenOutOfRange(Verb.PAXOS_PREPARE_REQ, Verb.FAILURE_RSP, new PrepareVerbHandler()); + } + + @Test + public void rejectProposeForTokenOutOfRange() throws Exception + { + rejectRequestForTokenOutOfRange(Verb.PAXOS_PROPOSE_REQ, Verb.FAILURE_RSP, new ProposeVerbHandler()); + } + + private void rejectRequestForTokenOutOfRange(Verb requestVerb, Verb responseVerb, AbstractPaxosVerbHandler handler) throws Exception + { + // reject a commit for a token the node neither owns nor is pending + ListenableFuture messageSink = registerOutgoingMessageSink(); + int messageId = randomInt(); + int key = 200; + Commit commit = commit(key); + handler.doVerb(Message.builder(requestVerb, commit).from(node1).withId(messageId).build()); + getAndVerifyResponse(messageSink, responseVerb, messageId, true); + } + + private void getAndVerifyResponse(ListenableFuture messageSink, + Verb verb, + int messageId, + boolean isOutOfRange) throws InterruptedException, ExecutionException, TimeoutException + { + MessageDelivery response = messageSink.get(100, TimeUnit.MILLISECONDS); + assertEquals(verb, response.message.verb()); + Assert.assertEquals(broadcastAddress, response.message.from()); + assertEquals(isOutOfRange, response.message.payload instanceof RequestFailureReason); + assertEquals(messageId, response.message.id()); + Assert.assertEquals(node1, response.to); + assertEquals(startingTotalMetricCount + (isOutOfRange ? 1 : 0), StorageMetrics.totalOpsForInvalidToken.getCount()); + assertEquals(startingKeyspaceMetricCount + (isOutOfRange ? 1 : 0), keyspaceMetricValue()); + } + + private static Commit commit(int key) + { + TableMetadata tmd = Schema.instance.getTableMetadata(KEYSPACE, TABLE); + return Commit.newPrepare(key(tmd, key), tmd, BallotGenerator.Global.nextBallot(Ballot.Flag.NONE)); + } + + private static long keyspaceMetricValue() + { + return Keyspace.open(KEYSPACE).metric.outOfRangeTokenPaxosRequests.getCount(); + } +} diff --git a/test/unit/org/apache/cassandra/service/reads/AbstractReadResponseTest.java b/test/unit/org/apache/cassandra/service/reads/AbstractReadResponseTest.java index ce0ef43826..7d22439d8d 100644 --- a/test/unit/org/apache/cassandra/service/reads/AbstractReadResponseTest.java +++ b/test/unit/org/apache/cassandra/service/reads/AbstractReadResponseTest.java @@ -31,6 +31,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; @@ -72,6 +73,7 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; @@ -148,7 +150,7 @@ public abstract class AbstractReadResponseTest .addPartitionKeyColumn("k", ByteType.instance) .addRegularColumn("m", MapType.getInstance(IntegerType.instance, IntegerType.instance, true)); - SchemaLoader.prepareServer(); + ServerTestUtils.prepareServerNoRegister(); SchemaLoader.createKeyspace(KEYSPACE1, KeyspaceParams.simple(2), builder1, builder2); SchemaLoader.createKeyspace(KEYSPACE3, KeyspaceParams.simple(4), builder3); @@ -161,11 +163,13 @@ public abstract class AbstractReadResponseTest cfs3 = ks3.getColumnFamilyStore(CF_STANDARD); cfm3 = cfs3.metadata(); m = cfm2.getColumn(new ColumnIdentifier("m", false)); + ServerTestUtils.markCMS(); } @Before public void setUp() throws Exception { + ServerTestUtils.resetCMS(); dk = Util.dk("key1"); nowInSec = FBUtilities.nowInSeconds(); } @@ -250,6 +254,7 @@ public abstract class AbstractReadResponseTest : ReadResponse.createRemoteDataResponse(data, repairedDataDigest, hasPendingRepair, command, fromVersion); return Message.builder(READ_REQ, response) .from(from) + .withEpoch(ClusterMetadata.current().epoch) .build(); } diff --git a/test/unit/org/apache/cassandra/service/reads/DataResolverTest.java b/test/unit/org/apache/cassandra/service/reads/DataResolverTest.java index 24504296ad..07b92a7d06 100644 --- a/test/unit/org/apache/cassandra/service/reads/DataResolverTest.java +++ b/test/unit/org/apache/cassandra/service/reads/DataResolverTest.java @@ -21,7 +21,7 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Iterator; -import java.util.UUID; +import java.util.function.BiFunction; import com.google.common.collect.Iterators; import com.google.common.collect.Sets; @@ -58,18 +58,23 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.locator.ReplicaUtils; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.net.*; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.reads.repair.ReadRepair; import org.apache.cassandra.service.reads.repair.RepairedDataTracker; import org.apache.cassandra.service.reads.repair.RepairedDataVerifier; import org.apache.cassandra.service.reads.repair.TestableReadRepair; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.Util.assertClustering; @@ -92,8 +97,6 @@ public class DataResolverTest extends AbstractReadResponseTest private EndpointsForRange makeReplicas(int num) { - StorageService.instance.getTokenMetadata().clearUnsafe(); - switch (num) { case 2: @@ -118,8 +121,9 @@ public class DataResolverTest extends AbstractReadResponseTest { InetAddressAndPort endpoint = InetAddressAndPort.getByAddress(new byte[]{ 127, 0, 0, (byte) (i + 1) }); replicas.add(ReplicaUtils.full(endpoint)); - StorageService.instance.getTokenMetadata().updateNormalToken(token = token.nextValidToken(), endpoint); - Gossiper.instance.initializeNodeUnsafe(endpoint, UUID.randomUUID(), 1); + NodeId node = Register.register(new NodeAddresses(endpoint)); + UnsafeJoin.unsafeJoin(node, Sets.newHashSet(token = token.nextValidToken())); + } catch (UnknownHostException e) { @@ -547,11 +551,12 @@ public class DataResolverTest extends AbstractReadResponseTest @Test public void testRepairRangeTombstoneBoundary() throws UnknownHostException { - testRepairRangeTombstoneBoundary(1, 0, 1); + EndpointsForRange replicas = makeReplicas(2); + testRepairRangeTombstoneBoundary(replicas, 1, 0, 1); readRepair.sent.clear(); - testRepairRangeTombstoneBoundary(1, 1, 0); + testRepairRangeTombstoneBoundary(replicas, 1, 1, 0); readRepair.sent.clear(); - testRepairRangeTombstoneBoundary(1, 1, 1); + testRepairRangeTombstoneBoundary(replicas, 1, 1, 1); } /** @@ -559,9 +564,8 @@ public class DataResolverTest extends AbstractReadResponseTest * same deletion on both side (while is useless but could be created by legacy code pre-CASSANDRA-13237 and could * thus still be sent). */ - private void testRepairRangeTombstoneBoundary(int timestamp1, int timestamp2, int timestamp3) throws UnknownHostException + private void testRepairRangeTombstoneBoundary(EndpointsForRange replicas, int timestamp1, int timestamp2, int timestamp3) throws UnknownHostException { - EndpointsForRange replicas = makeReplicas(2); DataResolver resolver = new DataResolver(command, plan(replicas, ALL), readRepair, nanoTime()); InetAddressAndPort peer1 = replicas.get(0).endpoint(); InetAddressAndPort peer2 = replicas.get(1).endpoint(); @@ -1322,7 +1326,15 @@ public class DataResolverTest extends AbstractReadResponseTest private ReplicaPlan.SharedForRangeRead plan(EndpointsForRange replicas, ConsistencyLevel consistencyLevel) { - return ReplicaPlan.shared(new ReplicaPlan.ForRangeRead(ks, ks.getReplicationStrategy(), consistencyLevel, ReplicaUtils.FULL_BOUNDS, replicas, replicas, 1)); + BiFunction, Token, ReplicaPlan.ForWrite> repairPlan = (self, t) -> ReplicaPlans.forReadRepair(self, ClusterMetadata.current(), ks, consistencyLevel, t, (i) -> true); + return ReplicaPlan.shared(new ReplicaPlan.ForRangeRead(ks, + ks.getReplicationStrategy(), + consistencyLevel, + ReplicaUtils.FULL_BOUNDS, + replicas, replicas, + 1, null, + repairPlan, + Epoch.EMPTY)); } private static void resolveAndConsume(DataResolver resolver) diff --git a/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java b/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java index 36dd6d9a5c..3428516730 100644 --- a/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java +++ b/test/unit/org/apache/cassandra/service/reads/DigestResolverTest.java @@ -33,6 +33,7 @@ import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.schema.TableMetadata; import static org.apache.cassandra.locator.ReplicaUtils.full; @@ -212,7 +213,7 @@ public class DigestResolverTest extends AbstractReadResponseTest private ReplicaPlan.SharedForTokenRead plan(ConsistencyLevel consistencyLevel, EndpointsForToken replicas) { - return ReplicaPlan.shared(new ReplicaPlan.ForTokenRead(ks, ks.getReplicationStrategy(), consistencyLevel, replicas, replicas)); + return ReplicaPlan.shared(new ReplicaPlan.ForTokenRead(ks, ks.getReplicationStrategy(), consistencyLevel, replicas, replicas, null, (self) -> null, Epoch.EMPTY)); } private void waitForLatch(CountDownLatch startlatch) diff --git a/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java b/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java index 6f6bf36cbf..239628a438 100644 --- a/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java +++ b/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java @@ -40,6 +40,7 @@ import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.RequestFailureReason; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.Verb; @@ -249,7 +250,7 @@ public class ReadExecutorTest MockSinglePartitionReadCommand(long timeout) { - super(false, 0, false, cfs.metadata(), 0, null, null, null, Util.dk("ry@n_luvs_teh_y@nk33z"), null, null, false); + super(cfs.metadata().epoch, false, 0, false, cfs.metadata(), 0, null, null, null, Util.dk("ry@n_luvs_teh_y@nk33z"), null, null, false); this.timeout = timeout; } @@ -273,6 +274,6 @@ public class ReadExecutorTest private ReplicaPlan.ForTokenRead plan(ConsistencyLevel consistencyLevel, EndpointsForToken natural, EndpointsForToken selected) { - return new ReplicaPlan.ForTokenRead(ks, ks.getReplicationStrategy(), consistencyLevel, natural, selected); + return new ReplicaPlan.ForTokenRead(ks, ks.getReplicationStrategy(), consistencyLevel, natural, selected, (cm) -> null, (self) -> null, Epoch.EMPTY); } } diff --git a/test/unit/org/apache/cassandra/service/reads/range/ReplicaPlanMergerTest.java b/test/unit/org/apache/cassandra/service/reads/range/ReplicaPlanMergerTest.java index b577678c3b..8d4fee6d18 100644 --- a/test/unit/org/apache/cassandra/service/reads/range/ReplicaPlanMergerTest.java +++ b/test/unit/org/apache/cassandra/service/reads/range/ReplicaPlanMergerTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -36,7 +37,11 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.StorageService; +import static org.apache.cassandra.ServerTestUtils.markCMS; +import static org.apache.cassandra.ServerTestUtils.recreateCMS; +import static org.apache.cassandra.ServerTestUtils.resetCMS; import static org.apache.cassandra.Util.testPartitioner; +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; import static org.apache.cassandra.db.ConsistencyLevel.ALL; import static org.apache.cassandra.db.ConsistencyLevel.ANY; import static org.apache.cassandra.db.ConsistencyLevel.EACH_QUORUM; @@ -62,10 +67,19 @@ public class ReplicaPlanMergerTest @BeforeClass public static void defineSchema() throws ConfigurationException { + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); SchemaLoader.prepareServer(); StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + recreateCMS(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(2)); keyspace = Keyspace.open(KEYSPACE); + markCMS(); + } + + @Before + public void before() + { + resetCMS(); } /** diff --git a/test/unit/org/apache/cassandra/service/reads/range/TokenUpdater.java b/test/unit/org/apache/cassandra/service/reads/range/TokenUpdater.java index 7419fbb5c9..491b0119e0 100644 --- a/test/unit/org/apache/cassandra/service/reads/range/TokenUpdater.java +++ b/test/unit/org/apache/cassandra/service/reads/range/TokenUpdater.java @@ -20,27 +20,28 @@ package org.apache.cassandra.service.reads.range; import java.net.UnknownHostException; import java.util.List; -import java.util.UUID; +import java.util.Set; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; import org.apache.cassandra.Util; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Token; -import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.utils.FBUtilities; /** * Test utility class to set the partitioning tokens in the cluster. * * The per-endpoint tokens to be set can be specified with the {@code withTokens} and {@code withKeys} methods. - * The {@link #update()} method will apply the changes, cleaning the previous token metadata info. It will also - * initialize Gossip in all the endpoints but the local node. + * The {@link #update()} method will apply the changes, cleaning the previous token metadata info. */ public class TokenUpdater { @@ -63,6 +64,13 @@ public class TokenUpdater return this; } + public TokenUpdater withTokens(InetAddressAndPort endpoint, Token... tokens) + { + for (Token token : tokens) + endpointTokens.put(endpoint, token); + return this; + } + public TokenUpdater withKeys(int... keys) { return withKeys(localEndpoint(), keys); @@ -94,13 +102,27 @@ public class TokenUpdater public TokenUpdater update() { - TokenMetadata tmd = StorageService.instance.getTokenMetadata(); - tmd.clearUnsafe(); - tmd.updateNormalTokens(endpointTokens); - endpointTokens.keySet() - .stream() - .filter(e -> !e.equals(localEndpoint())) - .forEach(e -> Gossiper.instance.initializeNodeUnsafe(e, UUID.randomUUID(), 1)); + for (InetAddressAndPort ep : endpointTokens.keySet()) + { + NodeId id = ClusterMetadata.current().directory.peerId(ep); + if (id == null) + ClusterMetadataTestHelper.register(ep); + + Set tokens = Sets.newHashSet(endpointTokens.get(ep)); + NodeState state = ClusterMetadata.current().directory.peerState(ep); + switch(state) + { + case REGISTERED: + ClusterMetadataTestHelper.join(ep, tokens); + break; + case JOINED: + // note: this would be illegal outside of tests, as move is restricted to single tokens + ClusterMetadataTestHelper.lazyMove(ep, tokens).prepareMove().startMove().midMove().finishMove(); + break; + default: + throw new IllegalStateException("Cannot update tokens for " + ep + " as it is in state: " + state); + } + } return this; } diff --git a/test/unit/org/apache/cassandra/service/reads/repair/AbstractReadRepairTest.java b/test/unit/org/apache/cassandra/service/reads/repair/AbstractReadRepairTest.java index 1d97aeff67..c63e487091 100644 --- a/test/unit/org/apache/cassandra/service/reads/repair/AbstractReadRepairTest.java +++ b/test/unit/org/apache/cassandra/service/reads/repair/AbstractReadRepairTest.java @@ -18,7 +18,6 @@ package org.apache.cassandra.service.reads.repair; -import java.nio.ByteBuffer; import java.util.List; import java.util.Set; import java.util.UUID; @@ -30,8 +29,10 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; -import org.apache.cassandra.dht.ByteOrderedPartitioner; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.ReplicaPlan; @@ -40,7 +41,6 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.ColumnIdentifier; @@ -66,18 +66,19 @@ import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaPlans; import org.apache.cassandra.locator.ReplicaUtils; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.net.Message; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.Tables; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.locator.Replica.fullReplica; +import static org.apache.cassandra.locator.ReplicaPlans.forReadRepair; import static org.apache.cassandra.locator.ReplicaUtils.FULL_RANGE; import static org.apache.cassandra.net.Verb.INTERNAL_RSP; import static org.apache.cassandra.utils.Clock.Global.nanoTime; @@ -213,7 +214,7 @@ public abstract class AbstractReadRepairTest static void configureClass(ReadRepairStrategy repairStrategy) throws Throwable { - SchemaLoader.loadSchema(); + ServerTestUtils.prepareServerNoRegister(); String ksName = "ks"; String ddl = String.format("CREATE TABLE tbl (k int primary key, v text) WITH read_repair='%s'", @@ -243,10 +244,10 @@ public abstract class AbstractReadRepairTest replicaPlan = replicaPlan(ConsistencyLevel.QUORUM, replicas); - StorageService.instance.getTokenMetadata().clearUnsafe(); - StorageService.instance.getTokenMetadata().updateNormalToken(ByteOrderedPartitioner.instance.getToken(ByteBuffer.wrap(new byte[] { 0 })), replica1.endpoint()); - StorageService.instance.getTokenMetadata().updateNormalToken(ByteOrderedPartitioner.instance.getToken(ByteBuffer.wrap(new byte[] { 1 })), replica2.endpoint()); - StorageService.instance.getTokenMetadata().updateNormalToken(ByteOrderedPartitioner.instance.getToken(ByteBuffer.wrap(new byte[] { 2 })), replica3.endpoint()); + ClusterMetadataTestHelper.addEndpoint(replica1.endpoint(), new BytesToken(new byte[] { 0 })); + ClusterMetadataTestHelper.addEndpoint(replica2.endpoint(), new BytesToken(new byte[] { 1 })); + ClusterMetadataTestHelper.addEndpoint(replica3.endpoint(), new BytesToken(new byte[] { 2 })); + Gossiper.instance.initializeNodeUnsafe(replica1.endpoint(), UUID.randomUUID(), 1); Gossiper.instance.initializeNodeUnsafe(replica2.endpoint(), UUID.randomUUID(), 1); Gossiper.instance.initializeNodeUnsafe(replica3.endpoint(), UUID.randomUUID(), 1); @@ -296,12 +297,16 @@ public abstract class AbstractReadRepairTest { Token token = readPlan.range().left.getToken(); EndpointsForToken pending = EndpointsForToken.empty(token); - return ReplicaPlans.forWrite(readPlan.keyspace(), - ConsistencyLevel.TWO, - liveAndDown.forToken(token), - pending, - replica -> true, - ReplicaPlans.writeReadRepair(readPlan)); + EndpointsForToken live = liveAndDown.forToken(token); + return new ReplicaPlan.ForWrite(readPlan.keyspace(), + readPlan.replicationStrategy(), + ConsistencyLevel.TWO, + pending, + live, + live, + readPlan.contacts().forToken(token), + (cm) -> null, + Epoch.EMPTY); } static ReplicaPlan.ForRangeRead replicaPlan(EndpointsForRange replicas, EndpointsForRange targets) { @@ -313,7 +318,16 @@ public abstract class AbstractReadRepairTest } static ReplicaPlan.ForRangeRead replicaPlan(Keyspace keyspace, ConsistencyLevel consistencyLevel, EndpointsForRange replicas, EndpointsForRange targets) { - return new ReplicaPlan.ForRangeRead(keyspace, keyspace.getReplicationStrategy(), consistencyLevel, ReplicaUtils.FULL_BOUNDS, replicas, targets, 1); + return new ReplicaPlan.ForRangeRead(keyspace, + keyspace.getReplicationStrategy(), + consistencyLevel, + ReplicaUtils.FULL_BOUNDS, + replicas, + targets, + 1, + null, + (self, token) -> forReadRepair(self, ClusterMetadata.current(), keyspace, consistencyLevel, token, (r) -> true), + Epoch.EMPTY); } public abstract InstrumentedReadRepair createInstrumentedReadRepair(ReadCommand command, ReplicaPlan.Shared replicaPlan, long queryStartNanoTime); diff --git a/test/unit/org/apache/cassandra/service/reads/repair/BlockingReadRepairTest.java b/test/unit/org/apache/cassandra/service/reads/repair/BlockingReadRepairTest.java index 3a938a29ec..36c37892a8 100644 --- a/test/unit/org/apache/cassandra/service/reads/repair/BlockingReadRepairTest.java +++ b/test/unit/org/apache/cassandra/service/reads/repair/BlockingReadRepairTest.java @@ -70,9 +70,9 @@ public class BlockingReadRepairTest extends AbstractReadRepairTest configureClass(ReadRepairStrategy.BLOCKING); } - private static InstrumentedReadRepairHandler createRepairHandler(Map repairs, ReplicaPlan.ForWrite writePlan) + private static InstrumentedReadRepairHandler createRepairHandler(Map repairs, ReplicaPlan.ForWrite forReadRepair) { - return new InstrumentedReadRepairHandler(repairs, writePlan); + return new InstrumentedReadRepairHandler(repairs, forReadRepair); } private static InstrumentedReadRepairHandler createRepairHandler(Map repairs) diff --git a/test/unit/org/apache/cassandra/service/reads/repair/DiagEventsBlockingReadRepairTest.java b/test/unit/org/apache/cassandra/service/reads/repair/DiagEventsBlockingReadRepairTest.java index 8399c838bc..c47ce4a848 100644 --- a/test/unit/org/apache/cassandra/service/reads/repair/DiagEventsBlockingReadRepairTest.java +++ b/test/unit/org/apache/cassandra/service/reads/repair/DiagEventsBlockingReadRepairTest.java @@ -179,9 +179,9 @@ public class DiagEventsBlockingReadRepairTest extends AbstractReadRepairTest return e -> candidates.contains(e); } - DiagnosticPartitionReadRepairHandler(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite writePlan) + DiagnosticPartitionReadRepairHandler(DecoratedKey key, Map repairs, ReplicaPlan.ForWrite forReadRepair) { - super(key, repairs, writePlan, isLocal()); + super(key, repairs, forReadRepair, isLocal()); DiagnosticEventService.instance().subscribe(PartitionRepairEvent.class, this::onRepairEvent); } diff --git a/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java b/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java index 1ae9d16105..9b07d45969 100644 --- a/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java +++ b/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java @@ -277,7 +277,8 @@ public class RepairedDataVerifierTest { StubReadCommand(int key, TableMetadata metadata, boolean isDigest) { - super(isDigest, + super(metadata.epoch, + isDigest, 0, false, metadata, diff --git a/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java b/test/unit/org/apache/cassandra/service/snapshot/MetadataSnapshotsTest.java similarity index 99% rename from test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java rename to test/unit/org/apache/cassandra/service/snapshot/MetadataSnapshotsTest.java index f9233bba8a..f2565ee3f5 100644 --- a/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java +++ b/test/unit/org/apache/cassandra/service/snapshot/MetadataSnapshotsTest.java @@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertTrue; -public class SnapshotManagerTest +public class MetadataSnapshotsTest { static long ONE_DAY_SECS = 86400; diff --git a/test/unit/org/apache/cassandra/streaming/StreamRateLimiterTest.java b/test/unit/org/apache/cassandra/streaming/StreamRateLimiterTest.java index 3b72c4d84d..32a2bb5f0c 100644 --- a/test/unit/org/apache/cassandra/streaming/StreamRateLimiterTest.java +++ b/test/unit/org/apache/cassandra/streaming/StreamRateLimiterTest.java @@ -20,7 +20,7 @@ package org.apache.cassandra.streaming; import java.net.UnknownHostException; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.ServerTestUtils; @@ -33,10 +33,10 @@ import static org.junit.Assert.assertTrue; public class StreamRateLimiterTest { - InetAddressAndPort REMOTE_PEER_ADDRESS; + static InetAddressAndPort REMOTE_PEER_ADDRESS; - @Before - public void prepareServer() throws UnknownHostException + @BeforeClass + public static void prepareServer() throws UnknownHostException { ServerTestUtils.daemonInitialization(); ServerTestUtils.prepareServer(); diff --git a/test/unit/org/apache/cassandra/streaming/StreamReaderTest.java b/test/unit/org/apache/cassandra/streaming/StreamReaderTest.java new file mode 100644 index 0000000000..79856ee153 --- /dev/null +++ b/test/unit/org/apache/cassandra/streaming/StreamReaderTest.java @@ -0,0 +1,547 @@ +/* + * 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.streaming; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import net.jpountz.lz4.LZ4Factory; +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.streaming.CassandraStreamHeader; +import org.apache.cassandra.db.streaming.CassandraStreamReader; +import org.apache.cassandra.db.streaming.IStreamReader; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.io.sstable.SSTableMultiWriter; +import org.apache.cassandra.io.sstable.SSTableSimpleIterator; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.sstable.format.big.BigFormat; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.TrackedDataInputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.net.AsyncStreamingOutputPlus; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.streaming.async.StreamCompressionSerializer; +import org.apache.cassandra.streaming.messages.StreamMessageHeader; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.TimeUUID; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.beginJoin; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.beginMove; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.setLocalTokens; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class StreamReaderTest +{ + private static final String TEST_NAME = "streamreader_test_"; + private static final String KEYSPACE = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + + @BeforeClass + public static void setupClass() throws Exception + { + DatabaseDescriptor.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + SchemaLoader.schemaDefinition(TEST_NAME); + ServerTestUtils.markCMS(); + } + + @Before + public void setup() throws Exception + { + // All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 400 + // Initially, the local node has no tokens so when indivividual test set owned tokens or + // pending ranges for the local node, they're always in relation to this. + // e.g. test calls setLocalTokens(100, 300) the ring now looks like + // peer -> (min, 0], (100, 200], (300, 400] + // local -> (0, 100], (200, 300], (400, max] + // + // Pending ranges are set in test using start/end pairs. + // Ring is initialised: + // peer -> (min, max] + // local -> (,] + // e.g. test calls setPendingRanges(0, 100, 200, 300) + // the pending ranges for local would be calculated as: + // local -> (0, 100], (200, 300] + clearAndSetPeerTokens(0, 200, 400); + } + + private static void clearAndSetPeerTokens(int...tokens) + { + ServerTestUtils.resetCMS(); + ClusterMetadataTestHelper.register(broadcastAddress); + Collection peerTokens = new HashSet<>(); + for (int token : tokens) + peerTokens.add(token(token)); + ClusterMetadataTestHelper.addEndpoint(node1, peerTokens); + } + + @Test + public void testReceiveWithNoOwnedRanges() throws Throwable + { + int[] tokens = {10, 20}; + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivingTableWithRangeContained() throws Throwable + { + int[] tokens = {10, 20}; + setLocalTokens(100); + + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivedTableLowestKeyBoundsExclusive() throws Throwable + { + // verify that ranges are left exclusive + int[] tokens = {0, 10}; + setLocalTokens(100); + + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivingTableRangeWithExactMatch() throws Throwable + { + // Because ranges are left exlusive, for the range (0, 100] the lowest permissable key is 1 + int[] tokens = {1, 100}; + setLocalTokens(100); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivingTableRangeLessThanOwned() throws Throwable + { + int[] tokens = {-100, 0}; + setLocalTokens(100); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivingTableRangeGreaterThanOwned() throws Throwable + { + int[] tokens = {101, 200}; + setLocalTokens(100); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedRangeReceivingTableRangeOverlappingOwned() throws Throwable + { + int[] tokens = {80, 120}; + setLocalTokens(100); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedWrappingRangeReceivingTableContainedBeforeMax() throws Throwable + { + int[] tokens = {110, 120}; + + // local node owns (min, 0] & (100, max], peer owns (0, 100] + clearAndSetPeerTokens(100); + setLocalTokens(0); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithSingleOwnedWrappingRangeReceivingTableContainedAfterMin() throws Throwable + { + int[] tokens = {-150, -140}; + + // local node owns (min, 0] & (100, max], peer owns (0, 100] + clearAndSetPeerTokens(100); + setLocalTokens(0); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithSingleOwnedWrappingRangeReceivingTableOverlappingUpward() throws Throwable + { + int[] tokens = {-10, 10}; + + // local node owns (min, 0] & (100, max], peer owns (0, 100] + clearAndSetPeerTokens(100); + setLocalTokens(0); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithSingleOwnedWrappingRangeReceivingTableOverlappingDownward() throws Throwable + { + int[] tokens = {90, 110}; + + // local node owns (min, 0] & (100, max], peer owns (0, 100] + clearAndSetPeerTokens(100); + setLocalTokens(0); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivingTableRangeContainedInFirstOwned() throws Throwable + { + int[] tokens = {10, 20}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivingTableRangeContainedInLastOwned() throws Throwable + { + int[] tokens = {450, 460}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivingTableRangeLessThanOwned() throws Throwable + { + int[] tokens = {510, 520}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivingTableRangeGreaterThanOwned() throws Throwable + { + int[] tokens = {-20, -10}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivingTableRangeOverlappingOwned() throws Throwable + { + int[] tokens = {80, 120}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesAllDisjointFromReceivingTableRange() throws Throwable + { + int[] tokens = {310, 320}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesDisjointSpannedByReceivingTableRange() throws Throwable + { + int[] tokens = {80, 320}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithMultipleOwnedRangesReceivedTableRangeExactMatch() throws Throwable + { + // bacause ranges are left exclusive, for the range (200, 300] the lowest permissable key is 201 + int[] tokens = {201, 300}; + setLocalTokens(100, 300, 500); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithOwnedRangeWrappingAndReceivedFileWhollyContained() throws Throwable + { + // peer -> (-100, 0], (100, 200], (300, 400] + // local -> (min, -100], (400, max] + setLocalTokens(-100); + int[] tokens = {-200, 500}; + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithOwnedRangeWrappingAndReceivedFilePartiallyContained() throws Throwable + { + // peer -> (-100, 0], (100, 200], (300, 400] + // local -> (min, -100], (400, max] + setLocalTokens(-100); + int[] tokens = {-200, 300}; + tryReceiveExpectingFailure(tokens); + } + + @Test + public void testReceiveWithOwnedRangeWrappingAndReceivedFileNotContained() throws Throwable + { + // peer -> (-100, 0], (100, 200], (300, 400] + // local -> (min, -100], (400, max] + setLocalTokens(-100); + int[] tokens = {0, 300}; + tryReceiveExpectingFailure(tokens); + } + + /***************************************************************************************** + * + * Unlike stream requests, when receiving streams we also have to consider pending ranges + * + ****************************************************************************************/ + + @Test + public void testReceiveWithSinglePendingRangeReceivingTableWithRangeContained() throws Throwable + { + int[] tokens = {10, 20}; + // start but don't finish the join process, emulating the previous pending ranges paradigm + beginJoin(100); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveWithMultiplePendingRangesReceivingTableRangeContainedInFirstOwned() throws Throwable + { + int[] tokens = {10, 20}; + // start but don't finish the join process, emulating the previous pending ranges paradigm + beginJoin(100, 300, 500); + tryReceiveExpectingSuccess(tokens); + } + + @Test + public void testReceiveNormalizesOwnedAndPendingRanges() throws Throwable + { + // Incoming file is not covered by either a single owned or pending range, + // but it is covered by the normalized set of both + int[] tokens = {90, 110}; + setLocalTokens(100); + // start but don't finish a token move, emulating the previous pending ranges paradigm + beginMove(120); + tryReceiveExpectingSuccess(tokens); + } + + public static StreamSession setupStreamingSessionForTest() + { + StreamCoordinator streamCoordinator = new StreamCoordinator(StreamOperation.REPAIR, 1, channelFactory, false, false, null, PreviewKind.NONE); + StreamResultFuture future = StreamResultFuture.createInitiator(nextTimeUUID(), StreamOperation.REPAIR, Collections.emptyList(), streamCoordinator); + + InetAddressAndPort peer = FBUtilities.getBroadcastAddressAndPort(); + streamCoordinator.addSessionInfo(new SessionInfo(peer, 0, peer, Collections.emptyList(), Collections.emptyList(), StreamSession.State.INITIALIZED, "")); + + StreamSession session = streamCoordinator.getOrCreateOutboundSession(peer); + session.init(future); + return session; + } + + private static void tryReceiveExpectingSuccess(int[] tokens) throws Throwable + { + StreamSession session = setupStreamingSessionForTest(); + StreamMessageHeader header = streamHeader(); + CassandraStreamHeader streamHeader = streamMessageHeader(tokens); + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + IStreamReader reader = streamReader(header, streamHeader, session); + StreamSummary streamSummary = new StreamSummary(streamHeader.tableId, 1, 0); + session.prepareReceiving(streamSummary); + reader.read(incomingStream(tokens)); + assertEquals(StorageMetrics.totalOpsForInvalidToken.getCount(), startMetricCount); + } + + private static void tryReceiveExpectingFailure(int[] tokens) throws Throwable + { + StreamSession session = setupStreamingSessionForTest(); + StreamMessageHeader header = streamHeader(); + CassandraStreamHeader streamHeader = streamMessageHeader(tokens); + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + StreamSummary streamSummary = new StreamSummary(streamHeader.tableId, 1, 0); + session.prepareReceiving(streamSummary); + try + { + IStreamReader reader = streamReader(header, streamHeader, session); + reader.read(incomingStream(tokens)); + fail("Expected StreamReceivedOfTokenRangeException"); + } + catch (StreamReceivedOutOfTokenRangeException e) + { + // expected + } + assertTrue(StorageMetrics.totalOpsForInvalidToken.getCount() > startMetricCount); + } + + + private static class BufferSupplier implements AsyncStreamingOutputPlus.BufferSupplier + { + ByteBuffer supplied; + + public ByteBuffer get(int capacity) throws IOException + { + supplied = ByteBuffer.allocateDirect(capacity); + return supplied; + } + + public ByteBuffer getSupplied() + { + return supplied; + } + + } + + private static DataInputPlus incomingStream(int...tokens) throws IOException + { + final net.jpountz.lz4.LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + + DataOutputBuffer out = new DataOutputBuffer(); + for (int token : tokens) + ByteBufferUtil.writeWithShortLength(ByteBufferUtil.bytes((long)token), out); + + int current_version = MessagingService.current_version; + + BufferSupplier bufferSupplier = new BufferSupplier(); + StreamCompressionSerializer.serialize(compressor, out.buffer(), current_version).write(bufferSupplier); + + return new DataInputBuffer(bufferSupplier.getSupplied(), false); + } + + private static IStreamReader streamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session) + { + return new KeysOnlyStreamReader(header, streamHeader, session); + } + + private static StreamMessageHeader streamHeader() + { + TableMetadata tmd = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata(); + int fakeSession = randomInt(9); + int fakeSeq = randomInt(9); + TimeUUID pendingRepair = null; + return new StreamMessageHeader(tmd.id, + null, + null, + true, + fakeSession, + fakeSeq, + System.currentTimeMillis(), + pendingRepair); + } + + private static CassandraStreamHeader streamMessageHeader(int...tokens) + { + TableMetadata tmd = Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE).metadata(); + Version version = BigFormat.getInstance().getLatestVersion(); + List fakeSections = new ArrayList<>(); + // each decorated key takes up (2 + 8) bytes, so this enables the + // StreamReader to calculate the expected number of bytes to read + fakeSections.add(new SSTableReader.PartitionPositionBounds(0L, (long)(tokens.length * 10) - 1)); + + return CassandraStreamHeader.builder() + .withTableId(tmd.id) + .withSerializationHeader(SerializationHeader.makeWithoutStats(tmd).toComponent()) + .withSSTableVersion(version) + .withSections(fakeSections) + .build(); + } + + // Simplifies generating test data as token == key (expects key to be an encoded long) + private static class FakeMurmur3Partitioner extends Murmur3Partitioner + { + public DecoratedKey decorateKey(ByteBuffer key) + { + return new BufferDecoratedKey(new LongToken(ByteBufferUtil.toLong(key)), key); + } + } + + // Stream reader which no-ops the reading/writing of the actual partition data. + // As we only care about keys/tokens here, we don't need to generate the rest + // of the sstable data to simulate a stream + private static class KeysOnlyStreamReader extends CassandraStreamReader + { + public KeysOnlyStreamReader(StreamMessageHeader header, CassandraStreamHeader streamHeader, StreamSession session) + { + super(header, streamHeader, session); + } + + protected SSTableMultiWriter createWriter(ColumnFamilyStore cfs, long totalSize, long repairedAt, TimeUUID pendingRepair, SSTableFormat format) throws IOException + { + return super.createWriter(cfs, totalSize, repairedAt, pendingRepair, format); + } + + @Override + protected StreamDeserializer getDeserializer(TableMetadata metadata, TrackedDataInputPlus in, Version inputVersion, StreamSession session, SSTableMultiWriter writer) throws IOException + { + return new TestStreamDeserializer(metadata, in, inputVersion, getHeader(metadata), session, writer); + } + + private static class TestStreamDeserializer extends CassandraStreamReader.StreamDeserializer + { + TestStreamDeserializer(TableMetadata metadata, DataInputPlus in, Version version, SerializationHeader header, StreamSession session, SSTableMultiWriter writer) throws IOException + { + super(metadata.unbuild().partitioner(new FakeMurmur3Partitioner()).build(), in, version, header, session, writer); + } + + @Override + protected void readPartition() throws IOException + { + // no-op, our dummy stream contains only decorated keys + partitionLevelDeletion = DeletionTime.LIVE; + iterator = new SSTableSimpleIterator.EmptySSTableSimpleIterator(metadata()); + } + + @Override + public Row staticRow() + { + return Rows.EMPTY_STATIC_ROW; + } + + @Override + public DeletionTime partitionLevelDeletion() + { + return DeletionTime.LIVE; + } + + + } + } + + static StreamingChannel.Factory channelFactory = (InetSocketAddress to, int messagingVersion, StreamingChannel.Kind kind) -> + { + throw new UnsupportedOperationException(); + }; +} diff --git a/test/unit/org/apache/cassandra/streaming/StreamSessionOwnedRangesTest.java b/test/unit/org/apache/cassandra/streaming/StreamSessionOwnedRangesTest.java new file mode 100644 index 0000000000..83f0b33dc7 --- /dev/null +++ b/test/unit/org/apache/cassandra/streaming/StreamSessionOwnedRangesTest.java @@ -0,0 +1,221 @@ +/* + * 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.streaming; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import io.netty.util.concurrent.Future; +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.metrics.StorageMetrics; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.streaming.messages.SessionFailedMessage; +import org.apache.cassandra.streaming.messages.StreamMessage; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; + +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.*; +import static org.apache.cassandra.net.MessagingService.current_version; +import static org.apache.cassandra.streaming.StreamingChannel.Factory.Global.streamingFactory; +import static org.apache.cassandra.streaming.messages.StreamMessage.Type.*; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.generateRangesAtEndpoint; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.setLocalTokens; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class StreamSessionOwnedRangesTest +{ + private static final String TEST_NAME = "stream_owned_ranges_test_"; + private static final String KEYSPACE = TEST_NAME + "cql_keyspace"; + private static final String TABLE = "table1"; + + static + { + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + } + + @BeforeClass + public static void setupClass() throws Exception + { + SchemaLoader.loadSchema(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.recreateCMS(); + SchemaLoader.schemaDefinition(TEST_NAME); + ClusterMetadataTestHelper.register(broadcastAddress); + ServerTestUtils.markCMS(); + StorageService.instance.unsafeSetInitialized(); + } + + @Before + public void setup() throws Exception + { + ServerTestUtils.resetCMS(); + // All tests suppose a 2 node ring, with the other peer having the tokens 0, 200, 300 + // Initially, the local node has no tokens so when indivividual test set owned tokens or + // pending ranges for the local node, they're always in relation to this. + // e.g. test calls setLocalTokens(100, 300) the ring now looks like + // peer -> (min, 0], (100, 200], (300, 400] + // local -> (0, 100], (200, 300], (400, max] + // + // Pending ranges are set in test using start/end pairs. + // Ring is initialised: + // peer -> (min, max] + // local -> (,] + // e.g. test calls setPendingRanges(0, 100, 200, 300) + // the pending ranges for local would be calculated as: + // local -> (0, 100], (200, 300] + ClusterMetadataTestHelper.addEndpoint(node1, Lists.newArrayList(token(0), token(200), token(400))); + } + + @Test + public void testPrepareWithAllRequestedRangesWithinOwned() + { + setLocalTokens(100); + InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort(); + Collection requests = streamRequests(generateRangesAtEndpoint(endpoint, 0, 10, 70, 80), + RangesAtEndpoint.empty(endpoint)); + + tryPrepareExpectingSuccess(requests); + } + + @Test + public void testPrepareWithAllRequestedRangesOutsideOwned() throws Exception + { + setLocalTokens(100); + InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort(); + + Collection requests = streamRequests(generateRangesAtEndpoint(endpoint, -20, -10, 110, 120, 310, 320), + RangesAtEndpoint.empty(endpoint)); + + tryPrepareExpectingFailure(requests); + } + + @Test + public void testPrepareWithSomeRequestedRangesOutsideOwned() throws Exception + { + setLocalTokens(100); + InetAddressAndPort endpoint = FBUtilities.getBroadcastAddressAndPort(); + + Collection requests = streamRequests(generateRangesAtEndpoint(endpoint, -20, -10, 30, 40, 310, 320), + RangesAtEndpoint.empty(endpoint)); + + tryPrepareExpectingFailure(requests); + } + + private static void tryPrepareExpectingSuccess(Collection requests) + { + final List sent = new ArrayList<>(); + StreamSession session = session(sent); + + sent.clear(); + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + + session.state(StreamSession.State.PREPARING); + session.prepareAsync(requests, Collections.emptySet()); + + assertEquals(2, sent.size()); + assertEquals(PREPARE_SYNACK, sent.get(0).type); + assertEquals(COMPLETE, sent.get(1).type); + + assertEquals(startMetricCount, StorageMetrics.totalOpsForInvalidToken.getCount()); + } + + private static void tryPrepareExpectingFailure(Collection requests) throws Exception + { + final List sent = new ArrayList<>(); + StreamSession session = session(sent); + sent.clear(); + long startMetricCount = StorageMetrics.totalOpsForInvalidToken.getCount(); + + session.state(StreamSession.State.PREPARING); + java.util.concurrent.Future f = session.prepare(requests, Collections.emptySet()); + Exception ex = f.get(); + assertNotNull(ex); + if (!(ex instanceof StreamRequestOutOfTokenRangeException)) + { // Unexpected exception + throw ex; + } + + // make sure we sent a SessionFailedMessage + assertEquals(1, sent.size()); + for (StreamMessage msg : sent) + assertTrue(msg instanceof SessionFailedMessage); + assertEquals(startMetricCount + 1, StorageMetrics.totalOpsForInvalidToken.getCount()); + } + + private static Collection streamRequests(RangesAtEndpoint fullRanges, + RangesAtEndpoint transientRanges) + { + return Collections.singleton(new StreamRequest(KEYSPACE, + fullRanges, + transientRanges, + Collections.singleton(TABLE))); + + } + + static StreamSession session(List sentMessages) + { + StreamCoordinator streamCoordinator = new StreamCoordinator(StreamOperation.REPAIR, 1, streamingFactory(), false, false, null, PreviewKind.NONE); + StreamResultFuture future = StreamResultFuture.createInitiator(nextTimeUUID(), StreamOperation.REPAIR, Collections.emptyList(), streamCoordinator); + + StreamSession session = new StreamSession(StreamOperation.REPAIR, + node1, + StreamReaderTest.channelFactory, + null, + current_version, + true, + 0, + null, + PreviewKind.NONE) + { + @Override + public void progress(String filename, ProgressInfo.Direction direction, long bytes, long delta, long total) + { + //no-op + } + + @Override + protected Future sendControlMessage(StreamMessage message) + { + sentMessages.add(message); + return ImmediateFuture.success(null); + } + + }; + session.init(future); + return session; + } +} diff --git a/test/unit/org/apache/cassandra/streaming/StreamSessionTest.java b/test/unit/org/apache/cassandra/streaming/StreamSessionTest.java index 44491d55e5..6eeb824b47 100644 --- a/test/unit/org/apache/cassandra/streaming/StreamSessionTest.java +++ b/test/unit/org/apache/cassandra/streaming/StreamSessionTest.java @@ -159,7 +159,7 @@ public class StreamSessionTest extends CQLTester { MockCFS(ColumnFamilyStore cfs, Directories dirs) { - super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata, dirs, false, false, true); + super(cfs.keyspace, cfs.getTableName(), Util.newSeqGen(), cfs.metadata.get(), dirs, false, false); } } diff --git a/test/unit/org/apache/cassandra/tcm/BootWithMetadataTest.java b/test/unit/org/apache/cassandra/tcm/BootWithMetadataTest.java new file mode 100644 index 0000000000..831204d7ac --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/BootWithMetadataTest.java @@ -0,0 +1,184 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Random; +import java.util.Set; +import java.util.function.Predicate; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.FileOutputStreamPlus; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.MembershipUtils; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.AddToCMS; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.serialization.VerboseMetadataSerializer; +import org.apache.cassandra.tcm.transformations.cms.FinishAddToCMS; +import org.apache.cassandra.tools.TransformClusterMetadataHelper; +import org.apache.cassandra.utils.FBUtilities; + +import static org.apache.cassandra.tcm.membership.MembershipUtils.nodeAddresses; +import static org.apache.cassandra.tcm.membership.MembershipUtils.randomEndpoint; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomPlacements; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomTokens; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.bootstrapAndJoin; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.bootstrapAndReplace; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.epoch; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.lockedRanges; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.move; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.unbootstrapAndLeave; +import static org.junit.Assert.assertEquals; + +public class BootWithMetadataTest +{ + private static final Logger logger = LoggerFactory.getLogger(BootWithMetadataTest.class); + /** + * Starts a test instance, then generates a randomised ClusterMetadata instance, writes it to + * file and finally re-initialises test instance's ClusterMetadataService. This exercises the + * same code paths as the counterpart test in o.a.c.distributed.test.log, but in a unit test + * it is much easier to generate random metadata, which need not actually be valid for a + * running service, so that we can better exercise the deserialisation code. + */ + @Test + public void bootFromExportedMetadataTest() throws Throwable + { + // sorting to preserve primary replicas requires real data in Directory + // and DataPlacements, this test uses completely random data so disable it + CassandraRelevantProperties.TCM_SORT_REPLICA_GROUPS.setBoolean(false); + // We will be re-intialising the ClusterMetadata and CMS, which requires + // us to disable MBean registration. This does not happen outside of tests + CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + // General test setup, no need to use Paxos for log commits or to replicate + // to (non-existent) peers + CassandraRelevantProperties.TCM_USE_ATOMIC_LONG_PROCESSOR.setBoolean(true); + CassandraRelevantProperties.TCM_USE_NO_OP_REPLICATOR.setBoolean(true); + + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + + Epoch epoch = epoch(new Random(System.nanoTime())); + ClusterMetadata first = ClusterMetadata.current(); + + for (int i = 0; i < 100; i++) + epoch = doTest(Epoch.create(epoch.getEpoch() + 100), first); + } + + private Epoch doTest(Epoch epoch, ClusterMetadata first) throws IOException + { + long seed = System.nanoTime(); + logger.info("STARTING TEST FROM EPOCH {}, SEED: {}", epoch, seed); + Random random = new Random(seed); + ClusterMetadata.Transformer t = first.transformer(); + // Generate a randomised CM in order to exercise the serde code + // TODO random schema + IPartitioner partitioner = first.partitioner; + Directory directory = first.directory; + int nodeCount = 10; + int tokensPerNode = 5; + for (int i = 0; i < nodeCount; i++) + directory = directory.with(nodeAddresses(random), new Location("DC1", "RACK1")); + t = t.with(directory); + + Iterator tokens = randomTokens(nodeCount * tokensPerNode, partitioner, random).iterator(); + Set perNodeTokens = new HashSet<>(tokensPerNode); + for (NodeId nodeId : directory.peerIds()) + { + perNodeTokens.clear(); + for (int i = 0; i < tokensPerNode; i++) + perNodeTokens.add(tokens.next()); + t = t.proposeToken(nodeId, perNodeTokens); + }; + + DataPlacements placements = randomPlacements(random); + t = t.with(placements); + t = t.with(lockedRanges(placements, random)); + + InProgressSequences seq = first.inProgressSequences; + seq = addSequence(seq, bootstrapAndJoin(partitioner, random, seq::contains)); + seq = addSequence(seq, bootstrapAndReplace(partitioner, random, seq::contains)); + seq = addSequence(seq, unbootstrapAndLeave(partitioner, random, seq::contains)); + seq = addSequence(seq, move(partitioner, random, seq::contains)); + seq = addSequence(seq, addToCMS(random, seq::contains)); + t = t.with(seq); + ClusterMetadata toWrite = t.build().metadata.forceEpoch(epoch).forcePeriod(999); + + // before exporting to file, make the local node the single CMS member in the CM instance + // as CMS membership is a requirement for re-initialising from file + toWrite = TransformClusterMetadataHelper.makeCMS(toWrite, FBUtilities.getBroadcastAddressAndPort()); + + // export the hand crafted CM to file + Path path = Files.createTempFile("clustermetadata", "dump"); + try (FileOutputStreamPlus out = new FileOutputStreamPlus(path)) + { + VerboseMetadataSerializer.serialize(ClusterMetadata.serializer, toWrite, out, NodeVersion.CURRENT_METADATA_VERSION); + } + String fileName = path.toString(); + + // now re-initialise the local CMS from the file + Startup.reinitializeWithClusterMetadata(fileName, p -> p, () -> {}); + + ClusterMetadata fromRead = ClusterMetadata.current(); + assertEquals(toWrite.schema, fromRead.schema); + assertEquals(toWrite.directory, fromRead.directory); + assertEquals(toWrite.tokenMap, fromRead.tokenMap); + assertEquals(toWrite.placements, fromRead.placements); + assertEquals(toWrite.lockedRanges, fromRead.lockedRanges); + assertEquals(toWrite.inProgressSequences, fromRead.inProgressSequences); + assertEquals(toWrite.extensions, fromRead.extensions); + + return fromRead.epoch; + } + + private InProgressSequences addSequence(InProgressSequences sequences, MultiStepOperation seq) + { + return sequences.with(seq.sequenceKey(), seq); + } + + private AddToCMS addToCMS(Random random, Predicate alreadyInUse) + { + NodeId node = MembershipUtils.node(random); + while (alreadyInUse.test(node)) + node = MembershipUtils.node(random); + + return new AddToCMS(epoch(random), + node, + Collections.singleton(randomEndpoint(random)), + new FinishAddToCMS(randomEndpoint(random))); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java new file mode 100644 index 0000000000..9896167a50 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm; + +import java.util.concurrent.ExecutionException; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.distributed.test.log.CMSTestBase; +import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.ownership.DataPlacement; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.getLeavePlan; +import static org.junit.Assert.assertTrue; + + +public class ClusterMetadataTest +{ + @BeforeClass + public static void beforeClass() + { + ServerTestUtils.prepareServerNoRegister(); + } + + @Before + public void before() throws ExecutionException, InterruptedException + { + ClusterMetadataService.unsetInstance(); + new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, new PlacementSimulator.SimpleReplicationFactor(3)); + } + + @Test + public void testWritePlacementAllSettledLeaving() throws ExecutionException, InterruptedException + { + for (int i = 1; i <= 4; i++) + { + ClusterMetadataTestHelper.register(i); + ClusterMetadataTestHelper.join(i, i); + } + ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareLeave(3)); + UnbootstrapAndLeave plan = getLeavePlan(3); + + ClusterMetadataService.instance().commit(plan.startLeave); + KeyspaceMetadata ksm = KeyspaceMetadata.create("ks", KeyspaceParams.simple(3)); + + DataPlacement writeAllSettled = ClusterMetadata.current().writePlacementAllSettled(ksm); + ClusterMetadataService.instance().commit(plan.midLeave); + ClusterMetadataService.instance().commit(plan.finishLeave); + + DataPlacement actualFinishedWritePlacements = ClusterMetadata.current().placements.get(ksm.params.replication); + + assertTrue(actualFinishedWritePlacements.difference(writeAllSettled).writes.removals.isEmpty()); + assertTrue(actualFinishedWritePlacements.difference(writeAllSettled).writes.additions.isEmpty()); + } + + @Test + public void testWritePlacementAllSettledJoining() + { + for (int i = 1; i <= 4; i++) + { + ClusterMetadataTestHelper.register(i); + ClusterMetadataTestHelper.join(i, i); + } + + ClusterMetadataTestHelper.register(10); + ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareJoin(10)); + + BootstrapAndJoin plan = ClusterMetadataTestHelper.getBootstrapPlan(10); + ClusterMetadataService.instance().commit(plan.startJoin); + KeyspaceMetadata ksm = KeyspaceMetadata.create("ks", KeyspaceParams.simple(3)); + DataPlacement writeAllSettled = ClusterMetadata.current().writePlacementAllSettled(ksm); + + ClusterMetadataService.instance().commit(plan.midJoin); + ClusterMetadataService.instance().commit(plan.finishJoin); + + DataPlacement actualFinishedWritePlacements = ClusterMetadata.current().placements.get(ksm.params.replication); + assertTrue(actualFinishedWritePlacements.difference(writeAllSettled).writes.removals.isEmpty()); + assertTrue(actualFinishedWritePlacements.difference(writeAllSettled).writes.additions.isEmpty()); + } + + @Test + public void testWritePlacementAllSettledMoving() + { + // todo + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTransformationTest.java b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTransformationTest.java new file mode 100644 index 0000000000..d25d02c80d --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTransformationTest.java @@ -0,0 +1,316 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.Collections; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import com.google.common.collect.Iterables; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.ClusterMetadata.Transformer.Transformed; +import org.apache.cassandra.tcm.extensions.EpochValue; +import org.apache.cassandra.tcm.extensions.ExtensionKey; +import org.apache.cassandra.tcm.extensions.ExtensionValue; +import org.apache.cassandra.tcm.extensions.IntValue; +import org.apache.cassandra.tcm.extensions.StringValue; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.MembershipUtils; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.InProgressSequences; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.mockito.Mockito; + +import static org.apache.cassandra.tcm.MetadataKeys.DATA_PLACEMENTS; +import static org.apache.cassandra.tcm.MetadataKeys.IN_PROGRESS_SEQUENCES; +import static org.apache.cassandra.tcm.MetadataKeys.LOCKED_RANGES; +import static org.apache.cassandra.tcm.MetadataKeys.NODE_DIRECTORY; +import static org.apache.cassandra.tcm.MetadataKeys.SCHEMA; +import static org.apache.cassandra.tcm.MetadataKeys.TOKEN_MAP; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomPlacements; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.affectedRanges; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.epoch; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ClusterMetadataTransformationTest +{ + @BeforeClass + public static void init() + { + ServerTestUtils.initSnitch(); + } + + long seed = System.nanoTime(); + Random random = new Random(seed); + + @Test + public void testModifyMembershipAndOwnership() + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + Transformed transformed = metadata.transformer().build(); + assertTrue(transformed.modifiedKeys.isEmpty()); + + NodeAddresses addresses = MembershipUtils.nodeAddresses(random); + transformed = metadata.transformer() + .register(addresses, new Location("dc1", "rack1"), NodeVersion.CURRENT) + .build(); + assertModifications(transformed, NODE_DIRECTORY); + NodeId n1 = transformed.metadata.directory.peerId(addresses.broadcastAddress); + + NodeAddresses updated = getNonConflictingAddresses(random, transformed.metadata.directory); + transformed = transformed.metadata.transformer().withNewAddresses(n1, updated).build(); + assertModifications(transformed, NODE_DIRECTORY); + + transformed = transformed.metadata.transformer().proposeToken(n1, Collections.singleton(token(100))).build(); + assertModifications(transformed, TOKEN_MAP); + + transformed = transformed.metadata.transformer().unproposeTokens(n1).build(); + assertModifications(transformed, NODE_DIRECTORY, TOKEN_MAP); + + transformed = transformed.metadata.transformer().proposeToken(n1, Collections.singleton(token(100))).join(n1).build(); + assertModifications(transformed, NODE_DIRECTORY, TOKEN_MAP); + + transformed = transformed.metadata.transformer().withNodeState(n1, NodeState.REGISTERED).build(); + assertModifications(transformed, NODE_DIRECTORY); + + transformed = transformed.metadata.transformer().addToRackAndDC(n1).build(); + assertModifications(transformed, NODE_DIRECTORY); + + NodeAddresses replaceAddresses = getNonConflictingAddresses(random, transformed.metadata.directory); + transformed = transformed.metadata.transformer() + .register(replaceAddresses, new Location("dc1", "rack1"), NodeVersion.CURRENT) + .build(); + NodeId n2 = transformed.metadata.directory.peerId(replaceAddresses.broadcastAddress); + transformed = transformed.metadata.transformer().replaced(n1, n2).build(); + assertModifications(transformed, NODE_DIRECTORY, TOKEN_MAP); + transformed = transformed.metadata.transformer().proposeRemoveNode(n2).build(); + assertModifications(transformed, TOKEN_MAP); + + NodeAddresses nextAddresses = getNonConflictingAddresses(random, transformed.metadata.directory); + transformed = transformed.metadata.transformer() + .register(nextAddresses, new Location("dc1", "rack1"), NodeVersion.CURRENT) + .build(); + NodeId n3 = transformed.metadata.directory.peerId(nextAddresses.broadcastAddress); + transformed = transformed.metadata.transformer() + .withNodeState(n3, NodeState.BOOTSTRAPPING) + .build(); + assertModifications(transformed, NODE_DIRECTORY); + transformed = transformed.metadata.transformer().left(n3).build(); + assertModifications(transformed, NODE_DIRECTORY, TOKEN_MAP); + } + + @Test + public void testModifySchema() + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + Transformed transformed = metadata.transformer().with(DistributedSchema.empty()).build(); + assertModifications(transformed, SCHEMA); + } + + @Test + public void testModifyPlacements() + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + // Initial state has DataPlacements.empty(), so supplying the same value results in no change + Transformed transformed = metadata.transformer().with(DataPlacements.empty()).build(); + assertModifications(transformed); + + DataPlacements trivial = DataPlacements.builder(1).with(ReplicationParams.simple(1), DataPlacement.empty()).build(); + transformed = transformed.metadata.transformer().with(trivial).build(); + assertModifications(transformed, DATA_PLACEMENTS); + } + + @Test + public void testModifyLockedRanges() + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + // Initial state has LockedRanges.EMPTY, so supplying the same value results in no change + Transformed transformed = metadata.transformer().with(LockedRanges.EMPTY).build(); + assertModifications(transformed); + + LockedRanges.AffectedRanges ranges = affectedRanges(randomPlacements(random), random); + LockedRanges trivial = transformed.metadata.lockedRanges.lock(LockedRanges.keyFor(epoch(random)), ranges); + transformed = transformed.metadata.transformer().with(trivial).build(); + assertModifications(transformed, LOCKED_RANGES); + } + + @Test + public void testModifyInProgressSequences() throws Throwable + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + // Initial state has InProgressSequences.EMPTY, so supplying the same value results in no change + Transformed transformed = metadata.transformer().with(InProgressSequences.EMPTY).build(); + assertModifications(transformed); + + InProgressSequences trivial = metadata.inProgressSequences.with(new NodeId(ThreadLocalRandom.current().nextInt()), + Mockito.mock(MultiStepOperation.class)); + transformed = transformed.metadata.transformer().with(trivial).build(); + assertModifications(transformed, IN_PROGRESS_SEQUENCES); + } + + @Test + public void testModifyExtendedMetadata() + { + long seed = System.nanoTime(); + Random r = new Random(seed); + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + ExtensionKey testKey = new ExtensionKey<>("foo.bar", EpochValue.class); + Epoch start = epoch(r); + + // write initial value with extension key + Transformed transformed = metadata.transformer().with(testKey, EpochValue.create(start)).build(); + assertModifications(transformed, testKey); + ExtensionValue stored = transformed.metadata.extensions.get(testKey); + Epoch actual = (Epoch) stored.getValue(); + assertEquals(start, actual); + + // overwrite initial value + Epoch updated = start.nextEpoch(); + transformed = transformed.metadata.transformer().with(testKey, EpochValue.create(updated)).build(); + assertModifications(transformed, testKey); + stored = transformed.metadata.extensions.get(testKey); + actual = (Epoch) stored.getValue(); + assertEquals(updated, actual); + + // don't overwrite using withIfAbsent + Epoch updatedAgain = updated.nextEpoch(); + transformed = transformed.metadata.transformer().withIfAbsent(testKey, EpochValue.create(updatedAgain)).build(); + assertModifications(transformed); + stored = transformed.metadata.extensions.get(testKey); + actual = (Epoch) stored.getValue(); + assertEquals(updated, actual); + + // remove value + transformed = transformed.metadata.transformer().without(testKey).build(); + assertModifications(transformed, testKey); + assertNull(transformed.metadata.extensions.get(testKey)); + + // now write usint withIfAbsent + transformed = transformed.metadata.transformer().withIfAbsent(testKey, EpochValue.create(updatedAgain)).build(); + assertModifications(transformed, testKey); + stored = transformed.metadata.extensions.get(testKey); + actual = (Epoch) stored.getValue(); + assertEquals(updatedAgain, actual); + } + + @Test + public void testRoundTripMetadataExtensions() throws IOException + { + ClusterMetadata metadata = new ClusterMetadata(Murmur3Partitioner.instance, Directory.EMPTY, DistributedSchema.empty()); + ExtensionKey epochKey = new ExtensionKey<>("test.epoch", EpochValue.class); + EpochValue e = EpochValue.create(epoch(random)); + + ExtensionKey intKey = new ExtensionKey<>("test.int", IntValue.class); + IntValue i = IntValue.create(random.nextInt()); + + ExtensionKey stringKey = new ExtensionKey<>("test.string", StringValue.class); + StringValue s = StringValue.create("" + random.nextInt()); + + Transformed transformed = metadata.transformer() + .with(epochKey, e) + .with(intKey, i) + .with(stringKey, s) + .build(); + assertModifications(transformed, epochKey, intKey, stringKey); + + DataOutputBuffer out = new DataOutputBuffer(); + ClusterMetadata.serializer.serialize(transformed.metadata, out, NodeVersion.CURRENT_METADATA_VERSION); + DataInputBuffer in = new DataInputBuffer(out.buffer(), false); + ClusterMetadata newMeta = ClusterMetadata.serializer.deserialize(in, NodeVersion.CURRENT_METADATA_VERSION); + + assertEquals(e.getValue(), newMeta.extensions.get(epochKey).getValue()); + assertEquals(i.getValue(), newMeta.extensions.get(intKey).getValue()); + assertEquals(s.getValue(), newMeta.extensions.get(stringKey).getValue()); + } + + private static NodeAddresses getNonConflictingAddresses(Random random, Directory directory) + { + outer: + while (true) + { + NodeAddresses addresses = MembershipUtils.nodeAddresses(random); + for (NodeAddresses existing : directory.addresses.values()) + if (addresses.conflictsWith(existing)) + continue outer; + + return addresses; + } + } + + private static void assertModifications(Transformed transformed, MetadataKey... expected) + { + assertEquals(expected.length, transformed.modifiedKeys.size()); + for (MetadataKey key : expected) + assertTrue(transformed.modifiedKeys.contains(key)); + + // anything modified by in this transformation, and therefore included in the modified keys, + // should have the same epoch as the CM itself. Anything not modified now must have a strictly + // earlier epoch + for (MetadataKey key : Iterables.concat(MetadataKeys.CORE_METADATA, transformed.metadata.extensions.keySet())) + { + MetadataValue value = valueFor(key, transformed.metadata); + if (transformed.modifiedKeys.contains(key)) + assertEquals(transformed.metadata.epoch, value.lastModified()); + else + assertTrue(transformed.metadata.epoch.isAfter(value.lastModified())); + } + } + + private static MetadataValue valueFor(MetadataKey key, ClusterMetadata metadata) + { + if (!MetadataKeys.CORE_METADATA.contains(key)) + { + assert key instanceof ExtensionKey; + return metadata.extensions.get((ExtensionKey)key); + } + + if (key == SCHEMA) + return metadata.schema; + else if (key == NODE_DIRECTORY) + return metadata.directory; + else if (key == TOKEN_MAP) + return metadata.tokenMap; + else if (key == DATA_PLACEMENTS) + return metadata.placements; + else if (key == LOCKED_RANGES) + return metadata.lockedRanges; + else if (key == IN_PROGRESS_SEQUENCES) + return metadata.inProgressSequences; + + throw new IllegalArgumentException("Unknown metadata key " + key); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/DiscoverySimulationTest.java b/test/unit/org/apache/cassandra/tcm/DiscoverySimulationTest.java new file mode 100644 index 0000000000..af24888858 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/DiscoverySimulationTest.java @@ -0,0 +1,186 @@ +/* + * 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.tcm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.NotImplementedException; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.ConnectionType; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.utils.concurrent.Future; + +public class DiscoverySimulationTest +{ + static + { + DatabaseDescriptor.daemonInitialization(); + } + + @BeforeClass + public static void setup() + { + ClusterMetadata initial = new ClusterMetadata(Murmur3Partitioner.instance); + LocalLog log = LocalLog.sync(new LocalLog.LogSpec().withInitialState(initial)); + log.ready(); + + ClusterMetadataService cms = new ClusterMetadataService(new UniformRangePlacement(), + MetadataSnapshots.NO_OP, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + false); + ClusterMetadataService.setInstance(cms); + } + + @Test + public void discoveryTest() throws Throwable + { + Map nodes = new HashMap<>(); + Map cluster = new HashMap<>(); + + Set seeds = new HashSet<>(); + seeds.add(InetAddressAndPort.getByName("127.0.100.1")); + seeds.add(InetAddressAndPort.getByName("127.0.100.100")); // add an unreachable node + for (int i = 1; i <= 10; i++) + { + InetAddressAndPort addr = InetAddressAndPort.getByName("127.0.100." + i); + FakeMessageDelivery messaging = new FakeMessageDelivery(cluster, addr); + cluster.put(addr, messaging); + Discovery discovery = new Discovery(() -> messaging, () -> Collections.unmodifiableSet(seeds), addr); + nodes.put(addr, discovery); + messaging.handlers.put(Verb.TCM_DISCOVER_REQ, discovery.requestHandler); + } + + List> futures = new ArrayList<>(); + for (Discovery value : nodes.values()) + futures.add(CompletableFuture.supplyAsync(() -> value.discover(5))); + + for (CompletableFuture future : futures) + future.get(); + + for (CompletableFuture future : futures) + Assert.assertEquals(nodes.keySet(), future.get().nodes()); + } + + /** + * A simple fake message delivery mechanism: holds all other nodes in a map, delivers messages to + * registered callbacks via this map, and times out messages to unknown nodes. + * + * Responses are delivered by checking if the verb is a response and finishing a callback. + */ + public static class FakeMessageDelivery implements MessageDelivery + { + private static AtomicInteger counter = new AtomicInteger(); + + private final Map cluster; + private final Map> callbacks; + private final Map> handlers; + private final InetAddressAndPort addr; + + public FakeMessageDelivery(Map cluster, + InetAddressAndPort addr) + { + this.cluster = cluster; + this.addr = addr; + this.callbacks = new ConcurrentHashMap<>(); + this.handlers = new HashMap<>(); + } + + public void deliverResponse(Message msg) + { + RequestCallback callback = (RequestCallback) callbacks.remove(msg.id()); + callback.onResponse(msg); + } + + public void send(Message message, InetAddressAndPort to) + { + if (message.verb().isResponse()) + { + cluster.get(to).deliverResponse(Message.forgeIdentityForTests(message, addr)); + return; + } + + throw new NotImplementedException(); + } + + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) + { + try + { + message = Message.forgeIdentityForTests(message, addr); + FakeMessageDelivery node = cluster.get(to); + if (node != null && + // Inject some failures + counter.incrementAndGet() % 2 != 0) + { + callbacks.put(message.id(), cb); + IVerbHandler handler = (IVerbHandler) node.handlers.get(message.verb()); + handler.doVerb(message); + } + else + { + cb.onFailure(to, RequestFailureReason.TIMEOUT); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) + { + throw new NotImplementedException(); + } + + public Future> sendWithResult(Message message, InetAddressAndPort to) + { + throw new NotImplementedException(); + } + + public void respond(V response, Message message) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/unit/org/apache/cassandra/tcm/LogStateTest.java b/test/unit/org/apache/cassandra/tcm/LogStateTest.java new file mode 100644 index 0000000000..309847d826 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/LogStateTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.extensions.ExtensionValue; +import org.apache.cassandra.tcm.listeners.MetadataSnapshotListener; +import org.apache.cassandra.tcm.log.LocalLog; +import org.apache.cassandra.tcm.log.LogStorage; +import org.apache.cassandra.tcm.ownership.UniformRangePlacement; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.SealPeriod; +import org.apache.cassandra.utils.FBUtilities; + +import static org.junit.Assert.assertEquals; + +public class LogStateTest +{ + @Before + public void setup() throws IOException + { + DatabaseDescriptor.daemonInitialization(); + StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.cleanupAndLeaveDirs(); + CommitLog.instance.start(); + LogStorage logStorage = LogStorage.SystemKeyspace; + MetadataSnapshots snapshots = new MetadataSnapshots.SystemKeyspaceMetadataSnapshots(); + ClusterMetadata initial = new ClusterMetadata(DatabaseDescriptor.getPartitioner()); + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(initial) + .withStorage(logStorage) + .withLogListener(new MetadataSnapshotListener()); + LocalLog log = LocalLog.sync(logSpec); + ClusterMetadataService cms = new ClusterMetadataService(new UniformRangePlacement(), + snapshots, + log, + new AtomicLongBackedProcessor(log), + Commit.Replicator.NO_OP, + false); + ClusterMetadataService.unsetInstance(); + ClusterMetadataService.setInstance(cms); + initial.schema.initializeKeyspaceInstances(DistributedSchema.empty()); + log.ready(); + log.bootstrap(FBUtilities.getBroadcastAddressAndPort()); + } + + @Test + public void testRevertEpoch() + { + ClusterMetadataService.instance().sealPeriod(); + List customEpochs = new ArrayList<>(40); + for (int i=0; i < 10; i++) + { + for (int j = 0; j < 4; j++) + { + ClusterMetadataService.instance() + .commit(new CustomTransformation(CustomTransformation.PokeInt.NAME, + new CustomTransformation.PokeInt((int) ClusterMetadata.current().epoch.getEpoch()))); + customEpochs.add(ClusterMetadata.current().epoch); + } + ClusterMetadataService.instance().commit(SealPeriod.instance); + } + + for (Epoch epoch : customEpochs) + { + ClusterMetadataService.instance().revertToEpoch(epoch); + ExtensionValue val = ClusterMetadata.current().extensions.get(CustomTransformation.PokeInt.METADATA_KEY); + if (val == null) + assertEquals(Epoch.create(2), epoch); // not yet any ints poked at epoch = 2 + else + // -1 since we poke the previous int to the extension: + assertEquals((int)(epoch.getEpoch() - 1), ClusterMetadata.current().extensions.get(CustomTransformation.PokeInt.METADATA_KEY).getValue()); + } + } +} diff --git a/test/unit/org/apache/cassandra/tcm/RecentlySealedPeriodsTest.java b/test/unit/org/apache/cassandra/tcm/RecentlySealedPeriodsTest.java new file mode 100644 index 0000000000..b0d236a67b --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/RecentlySealedPeriodsTest.java @@ -0,0 +1,112 @@ +/* + * 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.tcm; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class RecentlySealedPeriodsTest +{ + @Test + public void searchEmptyIndexReturnsMinPeriod() + { + Random random = new Random(System.nanoTime()); + RecentlySealedPeriods idx = RecentlySealedPeriods.EMPTY; + for (int i=0; i<1000; i++) + { + Epoch e = Epoch.create(Math.abs(random.nextLong())); + assertEquals(Sealed.EMPTY, idx.lookupEpochForSnapshot(e)); + assertEquals(Sealed.EMPTY, idx.lookupPeriodForReplication(e)); + } + } + + @Test + public void buildAndSearchIndex() + { + RecentlySealedPeriods idx = RecentlySealedPeriods.EMPTY; + for (int i=1; i<=10;i++) + idx = idx.with(Epoch.create((i*3)+1), i); + + assertEquals(10, idx.array().length); + // lowest indexed period is 1 + assertEquals(1, idx.array()[0].period); + // greatest indexed period is 10 + assertEquals(10, idx.array()[idx.array().length - 1].period); + + assertEquals(7, idx.lookupPeriodForReplication(Epoch.create(20)).period); + // looking an epoch which is the max in a sealed period, should return the next period (for replication purposes) + assertEquals(5, idx.lookupPeriodForReplication(Epoch.create(13)).period); + // lookup an epoch < the minimum indexed value + assertEquals(Sealed.EMPTY.period, idx.lookupPeriodForReplication(Epoch.create(1)).period); + // lookup an epoch > the maximum indexed value + assertEquals(Sealed.EMPTY.period, idx.lookupPeriodForReplication(Epoch.create(35)).period); + // lookup the max epoch in the lowest indexed period (should return the next-lowest) + assertEquals(2, idx.lookupPeriodForReplication(Epoch.create(4)).period); + + // add an additional entry + idx = idx.with(Epoch.create(100), 11); + // reassert the boundaries + assertEquals(10, idx.array().length); + // lowest indexed period is now 2 + assertEquals(2, idx.array()[0].period); + // greatest indexed period is now 11 + assertEquals(11, idx.array()[idx.array().length - 1].period); + + // lookup an epoch within the (new) maximum sealed period + assertEquals(11, idx.lookupPeriodForReplication(Epoch.create(76)).period); + + // repeat the previous lookups + assertEquals(7, idx.lookupPeriodForReplication(Epoch.create(20)).period); + assertEquals(5, idx.lookupPeriodForReplication(Epoch.create(13)).period); + + // lookup an epoch < the minimum indexed value + assertEquals(Sealed.EMPTY.period, idx.lookupPeriodForReplication(Epoch.create(1)).period); + // lookup an epoch > the maximum indexed value + assertEquals(Sealed.EMPTY.period, idx.lookupPeriodForReplication(Epoch.create(101)).period); + // the previous lower bound in the index should've been dropped out + // so repeating that lookup should now return the min period + assertEquals(Sealed.EMPTY.period, idx.lookupPeriodForReplication(Epoch.create(4)).period); + } + + @Test + public void sortAtBuildTime() + { + List keys = new ArrayList<>(10); + for (int i=1; i<=10; i++) + keys.add((long) (i * 10)); + Collections.shuffle(keys); + RecentlySealedPeriods idx = RecentlySealedPeriods.EMPTY; + for (Long epoch : keys) + idx = idx.with(Epoch.create(epoch), epoch/10); + + Sealed[] index = idx.array(); + for(int i=0; i allEndpoints = eps(endpointCount); + Set cms = new HashSet<>(allEndpoints.subList(0, 2)); + Set discovery = new HashSet<>(allEndpoints.subList(2, 4)); + RemoteProcessor.CandidateIterator iter = new RemoteProcessor.CandidateIterator(discovery, false); + + InetAddressAndPort returned = iter.next(); + assertTrue(discovery.contains(returned)); + iter.addCandidates(new Discovery.DiscoveredNodes(cms, Discovery.DiscoveredNodes.Kind.CMS_ONLY)); + returned = iter.next(); + assertFalse(discovery.contains(returned)); + assertTrue(cms.contains(returned)); + } + + @Test + public void timeoutTest() + { + // make sure that a node marked as timed out will not be returned until we've cycled through all other candidates + // when using the iterator in a RemoteProcessor::sendWithCallback call, the Backoff will trigger the breaking + // out of the cycle. + int endpointCount = 10; + List allEndpoints = eps(endpointCount); + Set discovery = new HashSet<>(allEndpoints.subList(0, 4)); + RemoteProcessor.CandidateIterator iter = new RemoteProcessor.CandidateIterator(discovery, false); + InetAddressAndPort timeout = iter.peek(); + for (int i = 1; i < 10; i++) + { + assertTrue(iter.hasNext()); + InetAddressAndPort returned = iter.next(); + if (returned.equals(timeout)) + { + iter.timeout(returned); + assertEquals(timeout, iter.peekLast()); + } + } + } + + @Test + public void notCMSTest() + { + // make sure that a node marked as notCMS will not be returned until we've cycled through all other candidates + // when using the iterator in a RemoteProcessor::sendWithCallback call, the Backoff will trigger the breaking + // out of the cycle. + int endpointCount = 10; + List allEndpoints = eps(endpointCount); + Set discovery = new HashSet<>(allEndpoints.subList(0, 4)); + RemoteProcessor.CandidateIterator iter = new RemoteProcessor.CandidateIterator(discovery, false); + InetAddressAndPort notcms = iter.peek(); + for (int i = 1; i < 10; i++) + { + assertTrue(iter.hasNext()); + InetAddressAndPort returned = iter.next(); + assertTrue(discovery.contains(returned)); + if (returned.equals(notcms)) + { + iter.notCms(returned); + assertEquals(notcms, iter.peekLast()); + } + } + } + + private List eps(int endpointCount) + { + List allEndpoints = new ArrayList<>(endpointCount); + for (int i = 0; i < endpointCount; i++) + { + InetAddressAndPort ep = InetAddressAndPort.getByNameUnchecked("127.0.0."+i); + allEndpoints.add(ep); + } + return allEndpoints; + } +} diff --git a/test/unit/org/apache/cassandra/tcm/compatibility/GossipHelperTest.java b/test/unit/org/apache/cassandra/tcm/compatibility/GossipHelperTest.java new file mode 100644 index 0000000000..329770db07 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/compatibility/GossipHelperTest.java @@ -0,0 +1,233 @@ +/* + * 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.tcm.compatibility; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.gms.ApplicationState; +import org.apache.cassandra.gms.EndpointState; +import org.apache.cassandra.gms.HeartBeatState; +import org.apache.cassandra.gms.VersionedValue; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.utils.CassandraVersion; + +import static org.apache.cassandra.gms.ApplicationState.*; +import static org.apache.cassandra.gms.VersionedValue.*; +import static org.apache.cassandra.locator.InetAddressAndPort.getByName; +import static org.apache.cassandra.tcm.compatibility.GossipHelper.fromEndpointStates; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class GossipHelperTest +{ + private static KeyspaceMetadata KSM, KSM_NTS; + private static final VersionedValue.VersionedValueFactory vvf = new VersionedValue.VersionedValueFactory(Murmur3Partitioner.instance); + + @BeforeClass + public static void beforeClass() + { + ServerTestUtils.prepareServerNoRegister(); + KSM = KeyspaceMetadata.create("ks", KeyspaceParams.simple(3)); + KSM_NTS = KeyspaceMetadata.create("ks_nts", KeyspaceParams.nts("dc1", 3, "dc2", 3)); + } + @Test + public void singleInstanceFromGossipTest() throws UnknownHostException + { + Keyspaces kss = Keyspaces.NONE.with(KSM); + DistributedSchema schema = new DistributedSchema(kss); + Map epstates = new HashMap<>(); + InetAddressAndPort endpoint = getByName("127.0.0.2"); // 127.0.0.1 is localhost, avoid that + InetAddressAndPort internal = getByName("127.0.0.3"); + InetAddressAndPort nativeAddress = getByName("127.0.0.4"); + UUID hostId = UUID.randomUUID(); + Token token = t(12345); + epstates.put(endpoint, epstate(internal, nativeAddress, token, hostId, "dc1")); + ClusterMetadata metadata = fromEndpointStates(epstates, Murmur3Partitioner.instance, schema); + NodeId nodeId = metadata.directory.peerId(endpoint); + assertEquals(hostId, metadata.directory.hostId(nodeId)); + assertEquals(token, metadata.tokenMap.tokens(nodeId).iterator().next()); + assertEquals("dc1", metadata.directory.location(nodeId).datacenter); + assertEquals("rack1", metadata.directory.location(nodeId).rack); + assertEquals(Version.OLD, metadata.directory.versions.get(nodeId).serializationVersion()); + assertEquals(new CassandraVersion("3.0.24"), metadata.directory.versions.get(nodeId).cassandraVersion); + assertEquals(internal, metadata.directory.addresses.get(nodeId).localAddress); + assertEquals(nativeAddress, metadata.directory.addresses.get(nodeId).nativeAddress); + + DataPlacements dp = metadata.placements; + assertEquals(1, dp.get(KSM.params.replication).reads.forToken(token).get().size()); + assertTrue(dp.get(KSM.params.replication).reads.forToken(token).get().contains(endpoint)); + assertEquals(1, dp.get(KSM.params.replication).writes.forToken(token).get().size()); + assertTrue(dp.get(KSM.params.replication).writes.forToken(token).get().contains(endpoint)); + } + + @Test + public void noRingChanges() throws UnknownHostException + { + Keyspaces kss = Keyspaces.NONE.with(KSM); + DistributedSchema schema = new DistributedSchema(kss); + Map epstates = new HashMap<>(); + for (String state : new String [] {STATUS_BOOTSTRAPPING, STATUS_LEAVING, STATUS_LEAVING, STATUS_BOOTSTRAPPING_REPLACE, REMOVING_TOKEN}) + { + epstates.put(getByName("127.0.0.2"), withState(state)); + try + { + fromEndpointStates(epstates, Murmur3Partitioner.instance, schema); + fail(); + } + catch (IllegalStateException e) + { + // expected + } + } + } + + @Test + public void testPlacements() throws UnknownHostException + { + int nodes = 10; + Keyspaces kss = Keyspaces.NONE.with(KSM_NTS); + DistributedSchema schema = new DistributedSchema(kss); + + Map endpoints = new HashMap<>(); + for (int i = 1; i < nodes; i++) + { + long t = i * 1000L; + endpoints.put(i, t(t)); + endpoints.put(++i, t(t + 1)); + } + Map epstates = new HashMap<>(); + for (Map.Entry entry : endpoints.entrySet()) + { + UUID hostId = UUID.randomUUID(); + int num = entry.getKey(); + InetAddressAndPort endpoint = getByName("127.0.0."+num); + epstates.put(endpoint, epstate(endpoint, endpoint, entry.getValue(), hostId, num % 2 == 1 ? "dc1" : "dc2")); + } + ClusterMetadata metadata = fromEndpointStates(epstates, Murmur3Partitioner.instance, schema); + verifyPlacements(endpoints, metadata); + } + + private static void verifyPlacements(Map endpoints, ClusterMetadata metadata) throws UnknownHostException + { + // quick check to make sure cm.placements is populated + for (Map.Entry entry : endpoints.entrySet()) + { + int num = entry.getKey(); + InetAddressAndPort endpoint = getByName("127.0.0."+num); + NodeId nodeId = metadata.directory.peerId(endpoint); + assertEquals(entry.getValue(), metadata.tokenMap.tokens(nodeId).iterator().next()); + } + + PlacementForRange reads = metadata.placements.get(KSM_NTS.params.replication).reads; + PlacementForRange writes = metadata.placements.get(KSM_NTS.params.replication).writes; + assertEquals(reads, writes); + // tokens are + // dc1: 1: 1000, 3: 3000, 5: 5000, 6: 7000, 7: 9000 + // dc2: 2: 1001, 4: 3001, 6: 5001, 8: 7001, 10:9001 + + // token 0 and 10000 should exist on nodes 1, 2, 3, 4, 5, 6 + for (Token t : new Token[] { t(0), t(10000) }) + verify(reads.forToken(t).get(), 1, 2, 3, 4, 5, 6); + + // token 6000 should be on nodes 7, 8, 9, 10, 1, 2; + verify(reads.forToken(t(6000)).get(), 7, 8, 9, 10, 1, 2); + // token 7000 should be on nodes 7, 8, 9, 10, 1, 2; + verify(reads.forToken(t(7000)).get(), 7, 8, 9, 10, 1, 2); + // token 5001 should be on nodes 6, 7, 8, 9, 10, 1 + verify(reads.forToken(t(5001)).get(), 6, 7, 8, 9, 10, 1); + } + + private static void verify(EndpointsForToken eps, int ... endpoints) throws UnknownHostException + { + assertEquals(endpoints.length, eps.size()); + for (int i : endpoints) + { + InetAddressAndPort ep = getByName("127.0.0." + i); + assertTrue("endpoint "+ep+" should be in " + eps, eps.contains(ep)); + } + } + + private static EndpointState epstate(InetAddressAndPort internalAddress, InetAddressAndPort nativeAddress, Token token, UUID hostId, String dc) + { + Map versionedValues = new EnumMap<>(ApplicationState.class); + versionedValues.put(STATUS_WITH_PORT, vvf.normal(Collections.singleton(token))); + versionedValues.put(TOKENS, vvf.tokens(Collections.singleton(token))); + versionedValues.put(HOST_ID, vvf.hostId(hostId)); + versionedValues.put(DC, vvf.datacenter(dc)); + versionedValues.put(RACK, vvf.datacenter("rack1")); + versionedValues.put(RELEASE_VERSION, vvf.releaseVersion("3.0.24")); + versionedValues.put(NATIVE_ADDRESS_AND_PORT, vvf.nativeaddressAndPort(nativeAddress)); + versionedValues.put(INTERNAL_ADDRESS_AND_PORT, vvf.internalAddressAndPort(internalAddress)); + return new EndpointState(new HeartBeatState(1, 1), versionedValues); + } + + private EndpointState withState(String status) throws UnknownHostException + { + EndpointState epstate = epstate(getByName("127.0.0.2"), getByName("127.0.0.2"), t(0), UUID.randomUUID(), "dc1"); + switch (status) + { + case STATUS_BOOTSTRAPPING: + epstate.addApplicationState(STATUS_WITH_PORT, vvf.bootstrapping(Collections.singleton(t(0)))); + break; + case STATUS_LEAVING: + epstate.addApplicationState(STATUS_WITH_PORT, vvf.leaving(Collections.singleton(t(0)))); + break; + case STATUS_MOVING: + epstate.addApplicationState(STATUS_WITH_PORT, vvf.moving(t(0))); + break; + case STATUS_BOOTSTRAPPING_REPLACE: + epstate.addApplicationState(STATUS_WITH_PORT, vvf.bootReplacing(InetAddress.getByName("127.0.0.3"))); + break; + case REMOVING_TOKEN: + epstate.addApplicationState(STATUS_WITH_PORT, vvf.removingNonlocal(UUID.randomUUID())); + break; + default: + throw new IllegalArgumentException("bad status: "+status); + } + return epstate; + } + + private static Token t(long l) + { + return new Murmur3Partitioner.LongToken(l); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/listeners/ClientNotificationListenerTest.java b/test/unit/org/apache/cassandra/tcm/listeners/ClientNotificationListenerTest.java new file mode 100644 index 0000000000..fde53deca9 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/listeners/ClientNotificationListenerTest.java @@ -0,0 +1,63 @@ +/* + * 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.tcm.listeners; + +import org.junit.Test; + +import org.apache.cassandra.tcm.membership.NodeState; + +import static org.apache.cassandra.tcm.listeners.ClientNotificationListener.ChangeType.*; +import static org.apache.cassandra.tcm.listeners.ClientNotificationListener.fromNodeStateTransition; +import static org.apache.cassandra.tcm.membership.NodeState.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ClientNotificationListenerTest +{ + @Test + public void testTransitions() + { + assertEquals(fromNodeStateTransition(null, JOINED), JOIN); + assertEquals(fromNodeStateTransition(REGISTERED, JOINED), JOIN); + assertEquals(fromNodeStateTransition(BOOTSTRAPPING, JOINED), JOIN); + + for (NodeState ns : NodeState.values()) + { + if (ns == LEFT) + continue; + assertEquals(ns.toString(), fromNodeStateTransition(ns, LEFT), LEAVE); + } + + assertEquals(fromNodeStateTransition(null, LEFT), LEAVE); + + // no client notifications when leaving/moving start: + assertNull(fromNodeStateTransition(JOINED, LEAVING)); + assertNull(fromNodeStateTransition(JOINED, MOVING)); + + // no client notifications until JOINED + assertNull(fromNodeStateTransition(null, BOOTSTRAPPING)); + assertNull(fromNodeStateTransition(null, REGISTERED)); + + // if LEAVING/MOVING is the first state we see for a node, assume it was JOINED before; + assertEquals(fromNodeStateTransition(null, LEAVING), JOIN); + assertEquals(fromNodeStateTransition(null, MOVING), JOIN); + + assertEquals(fromNodeStateTransition(MOVING, JOINED), MOVE); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/log/DistributedLogStateTest.java b/test/unit/org/apache/cassandra/tcm/log/DistributedLogStateTest.java new file mode 100644 index 0000000000..cc1d509979 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/log/DistributedLogStateTest.java @@ -0,0 +1,157 @@ +/* + * 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.tcm.log; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.SealPeriod; + +import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; +import static org.apache.cassandra.db.ColumnFamilyStore.FlushReason.UNIT_TESTS; +import static org.apache.cassandra.db.SystemKeyspace.METADATA_LOG; +import static org.apache.cassandra.db.SystemKeyspace.SEALED_PERIODS_TABLE_NAME; +import static org.apache.cassandra.schema.DistributedMetadataLogKeyspace.TABLE_NAME; +import static org.apache.cassandra.schema.SchemaConstants.METADATA_KEYSPACE_NAME; +import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class DistributedLogStateTest extends LogStateTestBase +{ + @BeforeClass + public static void setupClass() throws IOException + { + DatabaseDescriptor.daemonInitialization(); + StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.cleanupAndLeaveDirs(); + CommitLog.instance.start(); + ServerTestUtils.initCMS(); + } + + public DistributedLogStateTest(boolean truncateIndexTable, boolean truncateInMemoryIndex) + { + this.truncateIndexTable = truncateIndexTable; + this.truncateInMemoryIndex = truncateInMemoryIndex; + } + + @Override + LogStateSUT getSystemUnderTest(MetadataSnapshots snapshots) + { + return new LogStateSUT() + { + + // start test entries at FIRST + 1 as the pre-init transform is automatically inserted with Epoch.FIRST + Epoch currentEpoch = Epoch.FIRST; + Epoch nextEpoch; + long period = Period.FIRST; + long nextPeriod = period; + boolean applied; + + @Override + public void cleanup() throws IOException + { + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, METADATA_LOG).truncateBlockingWithoutSnapshot(); + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME).truncateBlockingWithoutSnapshot(); + ColumnFamilyStore.getIfExists(METADATA_KEYSPACE_NAME, TABLE_NAME).truncateBlockingWithoutSnapshot(); + Sealed.unsafeClearLookup(); + } + + @Override + public void insertRegularEntry() throws IOException + { + nextEpoch = currentEpoch.nextEpoch(); + boolean applied = DistributedMetadataLogKeyspace.tryCommit(new Entry.Id(currentEpoch.getEpoch()), + CustomTransformation.make((int) currentEpoch.getEpoch()), + currentEpoch, + nextEpoch, + period, + nextPeriod, + false); + assertTrue(applied); + currentEpoch = nextEpoch; + period = nextPeriod; + } + + @Override + public void sealPeriod() throws IOException + { + nextEpoch = currentEpoch.nextEpoch(); + applied = DistributedMetadataLogKeyspace.tryCommit(new Entry.Id(currentEpoch.getEpoch()), + SealPeriod.instance, + currentEpoch, + nextEpoch, + period, + period, + true); + assertTrue(applied); + // after appending a SealPeriod, move to a new partition + Sealed.recordSealedPeriod(period, nextEpoch); + nextPeriod++; + currentEpoch = nextEpoch; + // flush log table periodically so queries are served from disk + ColumnFamilyStore.getIfExists(DistributedMetadataLogKeyspace.Log.id).forceBlockingFlush(UNIT_TESTS); + } + + @Override + public LogState getLogState(Epoch since) + { + return DistributedMetadataLogKeyspace.getLogState(since, new DistributedMetadataLogKeyspace.DistributedTableLogReader(ConsistencyLevel.SERIAL), snapshots); + } + + @Override + public void dumpTables() throws IOException + { + UntypedResultSet r = executeInternal("SELECT period, epoch, entry_id, kind FROM system_cluster_metadata.distributed_metadata_log"); + r.forEach(row -> { + long p = row.getLong("period"); + long e = row.getLong("epoch"); + long i = row.getLong("entry_id"); + String s = row.getString("kind"); + System.out.println(String.format("(%d, %d, %d, %s)", p, e, i, s)); + }); + + String query = String.format("SELECT max_epoch, period FROM SYStem.metadata_sealed_periods"); + r = executeInternal(query); + r.forEach(row -> { + long p = row.getLong("period"); + long e = row.getLong("max_epoch"); + System.out.println(String.format("(%d, %d)", e, p)); + }); + } + }; + } +} diff --git a/test/unit/org/apache/cassandra/tcm/log/LocalLogTest.java b/test/unit/org/apache/cassandra/tcm/log/LocalLogTest.java new file mode 100644 index 0000000000..f3f03c9f04 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/log/LocalLogTest.java @@ -0,0 +1,256 @@ +/* + * 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.tcm.log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.concurrent.ExecutorPlus; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.dht.LocalPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.ForceSnapshot; +import org.apache.cassandra.tcm.transformations.SealPeriod; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.tcm.Epoch.EMPTY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class LocalLogTest +{ + @BeforeClass + public static void beforeClass() + { + DatabaseDescriptor.daemonInitialization(); + } + + @Test + public void appendToFillGapWithConsecutiveBufferedEntries() + { + LocalLog log = LocalLog.sync(new LocalLog.LogSpec().withInitialState(cm())); + log.ready(); + Epoch start = log.metadata().epoch; + assertEquals(EMPTY, start); + + Entry e1 = entry(1), e2 = entry(2), e3 = entry(3); + + log.append(e2); + Epoch tail = log.waitForHighestConsecutive().epoch; + + assertEquals(tail, start); + + log.append(e3); + tail = log.waitForHighestConsecutive().epoch; + assertEquals(tail, start); + + log.append(e1); + tail = log.waitForHighestConsecutive().epoch; + assertEquals(e3.epoch, tail); + } + + @Test + public void sealPeriodForceSnapshotCollisionWithGap() + { + LocalLog log = LocalLog.sync(new LocalLog.LogSpec().withInitialState(cm())); + log.ready(); + + List entries =new ArrayList<>(); + for (int i = 1; i <= 9; i++) + entries.add(entry(i)); + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(11), + SealPeriod.instance)); + + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(11), + new ForceSnapshot(new ClusterMetadata(new LocalPartitioner(IntegerType.instance)).forceEpoch(Epoch.create(11))))); + Collections.shuffle(entries); + log.append(entries); + + ClusterMetadata tail = log.waitForHighestConsecutive(); + + assertEquals(11, tail.epoch.getEpoch()); + } + + + @Test + public void multipleSnapshotEntries() + { + LocalLog log = LocalLog.sync(new LocalLog.LogSpec().withInitialState(cm())); + log.ready(); + + List entries =new ArrayList<>(); + for (int i = 1; i <= 9; i++) + entries.add(entry(i)); + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(11), + SealPeriod.instance)); + + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(11), + new ForceSnapshot(new ClusterMetadata(new LocalPartitioner(IntegerType.instance)).forceEpoch(Epoch.create(11))))); + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(21), + new ForceSnapshot(new ClusterMetadata(new LocalPartitioner(IntegerType.instance)).forceEpoch(Epoch.create(21))))); + entries.add(new Entry(Entry.Id.NONE, + Epoch.create(31), + new ForceSnapshot(new ClusterMetadata(new LocalPartitioner(IntegerType.instance)).forceEpoch(Epoch.create(31))))); + + Collections.shuffle(entries); + log.append(entries); + + ClusterMetadata tail = log.waitForHighestConsecutive(); + + assertEquals(31, tail.epoch.getEpoch()); + } + + @Test + public void appendFuzzTest() throws InterruptedException + { + for (int i = 0; i < 2000; i++) + { + singleAppendFuzzTest(); + } + } + + public void singleAppendFuzzTest() throws InterruptedException + { + long seed = System.nanoTime(); + Random random = new Random(seed); + int entryCount = random.nextInt(90) + 10; + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(entryCount); + for (int i = 0; i < entryCount; i++) + builder.add(entry(i + 1)); + ImmutableList entries = builder.build(); + Set submitted = ConcurrentHashMap.newKeySet(); + + int threads = 10; + CountDownLatch begin = CountDownLatch.newCountDownLatch(1); + CountDownLatch finish = CountDownLatch.newCountDownLatch(threads); + CountDownLatch finishReaders = CountDownLatch.newCountDownLatch(threads); + ExecutorPlus executor = executorFactory().configurePooled("APPENDER", threads * 2).build(); + LocalLog log = LocalLog.asyncForTests(cm()); + + List committed = new CopyOnWriteArrayList<>(); // doesn't need to be concurrent, since log is single-threaded + log.addListener((e, m) -> committed.add(e)); + + for (int i = 0; i < threads; i++) + { + executor.submit(() -> { + begin.awaitUninterruptibly(); + while (submitted.size() < entryCount) + { + // grab a random slice of up to 10 entries from the list and try to append them to the log + // end can be size + 1 because sublist is end-exclusive + int end = random.nextInt(entryCount + 1); + int start = Math.max(0, end - (random.nextInt(10) + 1)); + List toAppend = entries.subList(start, end); + log.append(toAppend); + submitted.addAll(toAppend); + } + finish.decrement(); + }); + } + + for (int i = 0; i < threads; i++) + { + executor.submit(() -> { + begin.awaitUninterruptibly(); + while (submitted.size() < entryCount) + { + Epoch waitFor = entries.get(random.nextInt(entries.size())).epoch; + try + { + Assert.assertTrue(log.awaitAtLeast(waitFor).epoch.isEqualOrAfter(waitFor)); + } + catch (InterruptedException | TimeoutException e) + { + // ignore + } + } + finishReaders.decrement(); + }); + } + + begin.decrement(); + finish.awaitUninterruptibly(); + + log.waitForHighestConsecutive(); + + assertEquals(entries.get(entries.size() - 1).epoch, log.metadata().epoch); + + if (!entries.equals(committed)) + fail("Committed list didn't match expected." + + "\n\tCommitted: " + toString(committed) + + "\n\tExpected : " + toString(entries) + + "\n\tPending: " + log.pendingBufferSize() + + "\n\tSeed: " + seed); + assertEquals(0, log.pendingBufferSize()); + + finishReaders.awaitUninterruptibly(); + + executor.shutdownNow(); + executor.awaitTermination(10, TimeUnit.SECONDS); + log.close(); + } + + public static String toString(Collection entries) + { + return entries.stream() + .map((e) -> Integer.toString(((CustomTransformation.PokeInt) ((CustomTransformation) e.transform).child()).v)) + .collect(Collectors.joining(",")); + } + + static Entry entry(int i) + { + return entry(i, i); + } + + static Entry entry(int i, long epoch) + { + return new Entry(new Entry.Id(i), + Epoch.create(epoch), + new CustomTransformation(CustomTransformation.PokeInt.NAME, new CustomTransformation.PokeInt(i))); + } + + static ClusterMetadata cm() + { + return new ClusterMetadata(Murmur3Partitioner.instance); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/tcm/log/LocalStorageLogStateTest.java b/test/unit/org/apache/cassandra/tcm/log/LocalStorageLogStateTest.java new file mode 100644 index 0000000000..d949380a58 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/log/LocalStorageLogStateTest.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.log; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Period; +import org.apache.cassandra.tcm.Sealed; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.SealPeriod; + +import static org.apache.cassandra.cql3.QueryProcessor.executeInternal; +import static org.apache.cassandra.db.SystemKeyspace.METADATA_LOG; +import static org.apache.cassandra.db.SystemKeyspace.SEALED_PERIODS_TABLE_NAME; +import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; + +@RunWith(Parameterized.class) +public class LocalStorageLogStateTest extends LogStateTestBase +{ + @BeforeClass + public static void setupClass() throws IOException + { + DatabaseDescriptor.daemonInitialization(); + StorageService.instance.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + CommitLog.instance.start(); + } + + public LocalStorageLogStateTest(boolean truncateIndexTable, boolean truncateInMemoryIndex) + { + this.truncateIndexTable = truncateIndexTable; + this.truncateInMemoryIndex = truncateInMemoryIndex; + } + + @Override + LogStateSUT getSystemUnderTest(MetadataSnapshots snapshots) + { + return new LogStateSUT() + { + SystemKeyspaceStorage storage = new SystemKeyspaceStorage(() -> snapshots); + Epoch epoch = Epoch.FIRST; + long period = Period.FIRST; + + @Override + public void cleanup() throws IOException + { + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, METADATA_LOG).truncateBlockingWithoutSnapshot(); + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME).truncateBlockingWithoutSnapshot(); + Sealed.unsafeClearLookup(); + } + + @Override + public void insertRegularEntry() throws IOException + { + // somewhat of a hack, but a "real" log as used by the DistributedMetadataKeyspace equivalent of this + // test will bootstrap the PreInitialize entry at Epoch.FIRST. SystemKeyspaceStorage doesn't do that, + // so fake an extra entry here to keep the test data in sync. + if (epoch.is(Epoch.FIRST)) + { + storage.append(period, new Entry(new Entry.Id(epoch.getEpoch()), epoch, CustomTransformation.make((int) epoch.getEpoch()))); + epoch = epoch.nextEpoch(); + } + storage.append(period, new Entry(new Entry.Id(epoch.getEpoch()), epoch, CustomTransformation.make((int) epoch.getEpoch()))); + epoch = epoch.nextEpoch(); + } + + @Override + public void sealPeriod() throws IOException + { + storage.append(period, new Entry(new Entry.Id(epoch.getEpoch()), epoch, SealPeriod.instance)); + Sealed.recordSealedPeriod(period, epoch); + epoch = epoch.nextEpoch(); + period += 1; + // required so we have a starting point for finding the right period to build + // replication from _if_ the max_epoch -> period table is lost + ClusterMetadataTestHelper.forceCurrentPeriodTo(period); + } + + @Override + public LogState getLogState(Epoch since) + { + return storage.getLogState(since); + } + + @Override + public void dumpTables() throws IOException + { + UntypedResultSet r = executeInternal("SELECT period, epoch, entry_id, kind FROM system.local_metadata_log"); + r.forEach(row -> { + long p = row.getLong("period"); + long e = row.getLong("epoch"); + long i = row.getLong("entry_id"); + String s = row.getString("kind"); + System.out.println(String.format("(%d, %d, %d, %s)", p, e, i, s)); + }); + + String query = String.format("SELECT max_epoch, period FROM system.metadata_sealed_periods"); + r = executeInternal(query); + r.forEach(row -> { + long p = row.getLong("period"); + long e = row.getLong("max_epoch"); + System.out.println(String.format("(%d, %d)", e, p)); + }); + } + }; + } + +} diff --git a/test/unit/org/apache/cassandra/tcm/log/LogListenerNotificationTest.java b/test/unit/org/apache/cassandra/tcm/log/LogListenerNotificationTest.java new file mode 100644 index 0000000000..d9a14e4e3f --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/log/LogListenerNotificationTest.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tcm.log; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataKey; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.extensions.IntValue; +import org.apache.cassandra.tcm.listeners.LogListener; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.sequences.LockedRanges; +import org.apache.cassandra.tcm.transformations.CustomTransformation; +import org.apache.cassandra.tcm.transformations.cms.PreInitialize; + +import static org.apache.cassandra.tcm.MetadataKeys.CORE_METADATA; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomPlacements; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.affectedRanges; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LogListenerNotificationTest +{ + @BeforeClass + public static void setup() + { + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + } + + @Test + public void testNotifyListener() + { + long seed = System.nanoTime(); + Random random = new Random(seed); + DataPlacements placements = randomPlacements(random); + + List input = new ArrayList<>(); + Epoch epoch = Epoch.FIRST; + for (int i = 0; i < 1000; i++) + { + epoch = epoch.nextEpoch(); + input.add(new Entry(new Entry.Id(i), + epoch, + new TestTransform(random.nextInt(), + affectedRanges(placements, random), + affectedMetadata(random)))); + } + + LogListener listener = new LogListener() + { + int counter = 0; + @Override + public void notify(Entry entry, Transformation.Result result) + { + Entry expected = input.get(counter); + assertEquals(expected, entry); + TestTransform transform = (TestTransform) expected.transform; + Set updatedMetadata = result.success().affectedMetadata; + assertEquals(transform.keys.size(), updatedMetadata.size()); + for (MetadataKey expectedKey : transform.keys) + assertTrue(updatedMetadata.contains(expectedKey)); + assertEquals(transform.ranges, result.success().affectedRanges); + + counter++; + } + }; + LocalLog.LogSpec logSpec = new LocalLog.LogSpec().withInitialState(cm()).withLogListener(listener); + LocalLog log = LocalLog.sync(logSpec); + log.append(new Entry(Entry.Id.NONE, Epoch.FIRST, PreInitialize.forTesting())); + log.ready(); + log.append(input); + } + + static ClusterMetadata cm() + { + return new ClusterMetadata(Murmur3Partitioner.instance); + } + + static Set affectedMetadata(Random random) + { + List src = new ArrayList<>(CORE_METADATA); + int required = random.nextInt(src.size()); + Set keys = new HashSet<>(); + while (keys.size() < required) + keys.add(src.get(random.nextInt(src.size()))); + return keys; + } + + static class TestTransform extends CustomTransformation.PokeInt + { + private final LockedRanges.AffectedRanges ranges; + private final ImmutableSet keys; + public TestTransform(int v, LockedRanges.AffectedRanges ranges, Set keys) + { + super(v); + this.ranges = ranges; + this.keys = ImmutableSet.copyOf(keys); + } + + @Override + public Result execute(ClusterMetadata prev) + { + return new Success(prev.transformer() + .with(CustomTransformation.PokeInt.METADATA_KEY, IntValue.create(v)) + .build().metadata, ranges, keys); + } + } +} diff --git a/test/unit/org/apache/cassandra/tcm/log/LogStateTestBase.java b/test/unit/org/apache/cassandra/tcm/log/LogStateTestBase.java new file mode 100644 index 0000000000..f552901ddd --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/log/LogStateTestBase.java @@ -0,0 +1,273 @@ +/* + * 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.tcm.log; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MetadataSnapshots; +import org.apache.cassandra.tcm.Sealed; + +import static org.apache.cassandra.db.SystemKeyspace.LAST_SEALED_PERIOD_TABLE_NAME; +import static org.apache.cassandra.db.SystemKeyspace.SEALED_PERIODS_TABLE_NAME; +import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public abstract class LogStateTestBase +{ + static int PERIOD_SIZE = 5; + static int NUM_PERIODS = 10; + static int EXTRA_ENTRIES = 2; + static int CURRENT_EPOCH = (NUM_PERIODS * PERIOD_SIZE) + EXTRA_ENTRIES; + static Sealed REAL_LAST_SEALED = new Sealed(NUM_PERIODS, Epoch.create(NUM_PERIODS * PERIOD_SIZE)); + + interface LogStateSUT + { + void cleanup() throws IOException; + void insertRegularEntry() throws IOException; + void sealPeriod() throws IOException; + LogState getLogState(Epoch since); + + // just for manually checking the test data + void dumpTables() throws IOException; + } + + abstract LogStateSUT getSystemUnderTest(MetadataSnapshots snapshots); + protected boolean truncateIndexTable; + protected boolean truncateInMemoryIndex; + + + @Parameterized.Parameters(name = "truncate index table: {0}, clear in-mem index: {1}") + public static Collection params() + { + return Arrays.asList(new Object[][] { + { true, true }, + { false, false }, + { true, false }, + { false, true } + }); + } + + @Before + public void initEntries() throws IOException + { + LogStateSUT sut = getSystemUnderTest(MetadataSnapshots.NO_OP); + sut.cleanup(); + for (long i = 0; i < NUM_PERIODS; i++) + { + // for the very first period (partition in the log table) we must write 1 fewer entries + // as the pre-init entry is automatically inserted with Epoch.FIRST when the table is empty + int entriesPerPeriod = PERIOD_SIZE - (i == 0 ? 2 : 1); + for (int j = 0; j < entriesPerPeriod; j++) + sut.insertRegularEntry(); + + sut.sealPeriod(); + } + + // Add 2 more Entries, which will be after the last sealed period. The point is to test what happens when + // we have to use the period to epoch reverse index, and the epochs are beyond the max indexed + for (int i = 0; i < 2; i++) + sut.insertRegularEntry(); + + if (truncateIndexTable) + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME).truncateBlockingWithoutSnapshot(); + + if (truncateInMemoryIndex) + Sealed.unsafeClearLookup(); + + sut.dumpTables(); + } + + static class TestSnapshots extends MetadataSnapshots.SystemKeyspaceMetadataSnapshots {}; + + static MetadataSnapshots withMissingSnapshot(Epoch expected) + { + return new TestSnapshots() + { + @Override + public ClusterMetadata getSnapshot(Epoch since) + { + assertEquals(expected, since); + return null; + } + }; + } + + static MetadataSnapshots withAvailableSnapshot(Epoch expected) + { + return new TestSnapshots() + { + @Override + public ClusterMetadata getSnapshot(Epoch since) + { + assertEquals(expected, since); + return ClusterMetadataTestHelper.minimalForTesting(Murmur3Partitioner.instance).forceEpoch(expected); + } + }; + } + + static MetadataSnapshots throwing() + { + return new TestSnapshots() + { + @Override + public ClusterMetadata getSnapshot(Epoch epoch) + { + fail("Did not expect to request a snapshot"); + return null; + } + }; + } + + @Test + public void sinceIsEmptyWithMissingSnapshot() + { + MetadataSnapshots missingSnapshot = withMissingSnapshot(REAL_LAST_SEALED.epoch); + LogState state = getSystemUnderTest(missingSnapshot).getLogState(Epoch.EMPTY); + assertNull(state.baseState); + assertReplication(state.transformations, 1, CURRENT_EPOCH); + } + + @Test + public void sinceIsEmptyWithAvailableSnapshot() + { + final Epoch expected = REAL_LAST_SEALED.epoch; + MetadataSnapshots withSnapshot = withAvailableSnapshot(expected); + LogState state = getSystemUnderTest(withSnapshot).getLogState(Epoch.EMPTY); + assertEquals(expected, state.baseState.epoch); + assertReplication(state.transformations, expected.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsBeforeLastSealedButMissingSnapshot() + { + MetadataSnapshots missingSnapshot = withMissingSnapshot(REAL_LAST_SEALED.epoch); + // an arbitrary epoch earlier than the last sealed + Epoch since = Epoch.create(((REAL_LAST_SEALED.period - 3) * PERIOD_SIZE ) + 2); + LogState state = getSystemUnderTest(missingSnapshot).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsBeforeLastSealedWithSnapshot() + { + final Epoch expected = REAL_LAST_SEALED.epoch; + MetadataSnapshots withSnapshot = withAvailableSnapshot(expected); + // an arbitrary epoch earlier than the last sealed + Epoch since = Epoch.create(((REAL_LAST_SEALED.period - 3) * PERIOD_SIZE ) + 2); + LogState state = getSystemUnderTest(withSnapshot).getLogState(since); + assertEquals(expected, state.baseState.epoch); + assertReplication(state.transformations, expected.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsMaxInLastSealedWithSnapshot() + { + // the max epoch in the last sealed period(but not the current highest epoch) + final Epoch since = REAL_LAST_SEALED.epoch; + MetadataSnapshots withSnapshot = withAvailableSnapshot(since); + LogState state = getSystemUnderTest(withSnapshot).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsMaxInLastSealedButMissingSnapshot() + { + // the max epoch in the last sealed period(but not the current highest epoch) + final Epoch since = REAL_LAST_SEALED.epoch; + MetadataSnapshots missingSnapshot = withMissingSnapshot(since); + LogState state = getSystemUnderTest(missingSnapshot).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsAfterLastSealed() + { + MetadataSnapshots snapshots = throwing(); + // an arbitrary epoch later than the last sealed (but not the current highest epoch) + Epoch since = Epoch.create(CURRENT_EPOCH - 1); + LogState state = getSystemUnderTest(snapshots).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceIsMaxAfterLastSealed() + { + MetadataSnapshots snapshots = throwing(); + // the current highest epoch, which > the max epoch in the last sealed period + Epoch since = Epoch.create(CURRENT_EPOCH); + LogState state = getSystemUnderTest(snapshots).getLogState(since); + assertNull(state.baseState); + assertTrue(state.transformations.isEmpty()); + } + + @Test + public void sinceArbitraryEpochWithoutAnySealed() + { + Sealed.unsafeClearLookup(); + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, SEALED_PERIODS_TABLE_NAME).truncateBlockingWithoutSnapshot(); + ColumnFamilyStore.getIfExists(SYSTEM_KEYSPACE_NAME, LAST_SEALED_PERIOD_TABLE_NAME).truncateBlockingWithoutSnapshot(); + MetadataSnapshots snapshots = throwing(); + Epoch since = Epoch.create(35); + LogState state = getSystemUnderTest(snapshots).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + @Test + public void sinceArbitraryEpochWithSealedButMissingSnapshot() + { + Epoch since = Epoch.create(35); + Epoch expected = REAL_LAST_SEALED.epoch; + MetadataSnapshots missingSnapshot = withMissingSnapshot(expected); + LogState state = getSystemUnderTest(missingSnapshot).getLogState(since); + assertNull(state.baseState); + assertReplication(state.transformations, since.nextEpoch().getEpoch(), CURRENT_EPOCH); + } + + private void assertReplication(Replication replication, long min, long max) + { + int idx = 0; + for (long i = min; i <= max; i++) + { + Entry e = replication.entries().get(idx); + assertEquals(e.epoch.getEpoch(), i); + idx++; + } + assertEquals(idx, replication.entries().size()); + } + +} diff --git a/test/unit/org/apache/cassandra/tcm/membership/MembershipUtils.java b/test/unit/org/apache/cassandra/tcm/membership/MembershipUtils.java new file mode 100644 index 0000000000..aebe443529 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/membership/MembershipUtils.java @@ -0,0 +1,60 @@ +/* + * 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.tcm.membership; + +import java.net.UnknownHostException; +import java.util.Random; + +import org.apache.cassandra.locator.InetAddressAndPort; + +public class MembershipUtils +{ + public static NodeAddresses nodeAddresses(Random random) + { + InetAddressAndPort endpoint = randomEndpoint(random); + return new NodeAddresses(endpoint); + } + + public static InetAddressAndPort randomEndpoint(Random random) + { + return endpoint(random.nextInt(254) + 1); + } + + public static InetAddressAndPort endpoint(int i) + { + return endpoint((byte)i); + } + + public static InetAddressAndPort endpoint(byte i) + { + try + { + return InetAddressAndPort.getByAddress(new byte[]{ 127, 0, 0, i }); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + public static NodeId node(Random r) + { + return new NodeId(r.nextInt(1000)); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ownership/DeltaMapTest.java b/test/unit/org/apache/cassandra/tcm/ownership/DeltaMapTest.java new file mode 100644 index 0000000000..3b2e014365 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ownership/DeltaMapTest.java @@ -0,0 +1,217 @@ +/* + * 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.tcm.ownership; + +import org.junit.Test; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; + +import static org.apache.cassandra.tcm.membership.MembershipUtils.endpoint; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.emptyReplicas; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.transientReplicas; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.fullReplicas; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DeltaMapTest +{ + private static final ReplicationParams key = ReplicationParams.simple(1); + private static final InetAddressAndPort P1 = endpoint(1); + private static final InetAddressAndPort P2 = endpoint(2); + private static final InetAddressAndPort P3 = endpoint(3); + private static final InetAddressAndPort P4 = endpoint(4); + private static final Range R1 = new Range<>(token(0), token(100)); + private static final Range R2 = new Range<>(token(100), token(200)); + private static final Range R_INT = new Range<>(token(50), token(150)); + + + @Test + public void mergeDisjointDeltas() + { + // Combine 2 Deltas with disjoint removals (and no additions), for the same ReplicationParams. + // Verify that the resulting merged Delta contains the removals/additions from both. + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P2, R2); + + Delta d1 = new Delta(group1, emptyReplicas()); + Delta d2 = new Delta(group2, emptyReplicas()); + PlacementDeltas.PlacementDelta merged = PlacementDeltas.builder(1) + .put(key, new PlacementDeltas.PlacementDelta(d1, d1)) + .put(key, new PlacementDeltas.PlacementDelta(d2, d2)) + .build() + .get(key); + + for (Delta delta : new Delta[]{ merged.reads, merged.writes }) + { + assertTrue(delta.additions.isEmpty()); + assertEquals(group1.get(P1), delta.removals.get(P1)); + assertEquals(group2.get(P2), delta.removals.get(P2)); + } + } + + @Test + public void mergeDisjointReplicasForSameEndpoint() + { + // Combine 2 Deltas which both contain removals for the same endpoint, but for disjoint ranges. + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P1, R2); + + Delta d1 = new Delta(group1, emptyReplicas()); + Delta d2 = new Delta(group2, emptyReplicas()); + PlacementDeltas.PlacementDelta merged = PlacementDeltas.builder(1) + .put(key, new PlacementDeltas.PlacementDelta(d1, d1)) + .put(key, new PlacementDeltas.PlacementDelta(d2, d2)) + .build() + .get(key); + + for (Delta delta : new Delta[]{ merged.reads, merged.writes }) + { + assertEquals(1, delta.removals.keySet().size()); + RangesAtEndpoint mergedGroup = delta.removals.get(P1); + + assertEquals(2, mergedGroup.size()); + group1.flattenValues().forEach(r -> assertTrue(mergedGroup.contains(r))); + group2.flattenValues().forEach(r -> assertTrue(mergedGroup.contains(r))); + } + } + + @Test + public void mergeIdenticalReplicasForSameEndpoint() + { + // Combine 2 Deltas which both contain identical removals for the same endpoint. + // Effectively a noop. + RangesByEndpoint group1 = fullReplicas(P1, R1); + + Delta d1 = new Delta(group1, emptyReplicas()); + Delta d2 = new Delta(group1, emptyReplicas()); + PlacementDeltas.PlacementDelta merged = PlacementDeltas.builder(1) + .put(key, new PlacementDeltas.PlacementDelta(d1, d1)) + .put(key, new PlacementDeltas.PlacementDelta(d2, d2)) + .build() + .get(key); + + for (Delta delta : new Delta[]{ merged.reads, merged.writes }) + { + assertEquals(1, delta.removals.keySet().size()); + RangesAtEndpoint mergedGroup = delta.removals.get(P1); + assertEquals(1, mergedGroup.size()); + group1.flattenValues().forEach(r -> assertTrue(mergedGroup.contains(r))); + } + } + + @Test + public void mergeIntersectingReplicasForSameEndpoint() + { + // Combine 2 Deltas which both contain replicas for a common endpoint, but with intersecting ranges. + // TODO there isn't an obvious reason to support this, so perhaps we should be conservative and + // explicitly reject it + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P1, R_INT); + + Delta d1 = new Delta(group1, emptyReplicas()); + Delta d2 = new Delta(group2, emptyReplicas()); + PlacementDeltas.PlacementDelta merged = PlacementDeltas.builder(1) + .put(key, new PlacementDeltas.PlacementDelta(d1, d1)) + .put(key, new PlacementDeltas.PlacementDelta(d2, d2)) + .build().get(key); + + for (Delta delta : new Delta[]{ merged.reads, merged.writes }) + { + assertEquals(1, delta.removals.keySet().size()); + RangesAtEndpoint mergedGroup = delta.removals.get(P1); + assertEquals(2, mergedGroup.size()); + group1.flattenValues().forEach(r -> assertTrue(mergedGroup.contains(r))); + group2.flattenValues().forEach(r -> assertTrue(mergedGroup.contains(r))); + } + } + + @Test + public void invertSingleDelta() + { + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P2, R2); + + Delta d1 = new Delta(group1, group2); + Delta d2 = new Delta(group2, group1); + + assertEquals(d1, d2.invert()); + assertEquals(d2, d2.invert().invert()); + } + + @Test + public void invertEmptyDelta() + { + Delta d = Delta.empty(); + assertEquals(d, d.invert()); + } + + @Test + public void invertPartiallyEmptyDelta() + { + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P2, R1); + + Delta additions = new Delta(emptyReplicas(), group1); + Delta inverted = additions.invert(); + assertEquals(RangesByEndpoint.EMPTY, inverted.additions); + assertEquals(additions.additions, inverted.removals); + + Delta removals = new Delta(group2, emptyReplicas()); + inverted = removals.invert(); + assertEquals(RangesByEndpoint.EMPTY, inverted.removals); + assertEquals(removals.removals, inverted.additions); + } + + @Test + public void invertPlacementDelta() + { + RangesByEndpoint group1 = fullReplicas(P1, R1); + RangesByEndpoint group2 = fullReplicas(P2, R1); + Delta d1 = new Delta(group1, group2); + + RangesByEndpoint group3 = fullReplicas(P3, R1); + RangesByEndpoint group4 = fullReplicas(P4, R2); + Delta d2 = new Delta(group3, group4); + + PlacementDeltas.PlacementDelta pd1 = new PlacementDeltas.PlacementDelta(d1,d2); + PlacementDeltas.PlacementDelta pd2 = new PlacementDeltas.PlacementDelta(d1.invert(), d2.invert()); + assertEquals(pd2, pd1.invert()); + } + + @Test + public void testMerge() + { + // delta to remove transient replica and add trivial replica + Delta toFinal = new Delta(transientReplicas(P1, R1), fullReplicas(P1, R1)); + // delta to remove trivial replica + Delta toMerge = new Delta(fullReplicas(P1, R1), RangesByEndpoint.EMPTY); + // merged should contain only the transient replica removal + Delta merged = toMerge.merge(toFinal); + assertEquals(0, merged.additions.get(P1).size()); + assertEquals(1, merged.removals.get(P1).size()); + assertTrue(merged.removals.get(P1).contains(Replica.transientReplica(P1, R1))); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ownership/OwnershipUtils.java b/test/unit/org/apache/cassandra/tcm/ownership/OwnershipUtils.java new file mode 100644 index 0000000000..3c4ea14a08 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ownership/OwnershipUtils.java @@ -0,0 +1,242 @@ +/* + * 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.tcm.ownership; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.dht.ByteOrderedPartitioner; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.RangesAtEndpoint; +import org.apache.cassandra.locator.RangesByEndpoint; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.locator.SimpleStrategy; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.tcm.Epoch; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.broadcastAddress; +import static org.apache.cassandra.tcm.membership.MembershipUtils.randomEndpoint; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +public class OwnershipUtils +{ + public static Token token(long t) + { + return new Murmur3Partitioner.LongToken(t); + } + + public static RangesByEndpoint emptyReplicas() + { + return new RangesByEndpoint.Builder().build(); + } + + public static RangesByEndpoint fullReplicas(InetAddressAndPort endpoint, Range range) + { + RangesByEndpoint.Builder b = new RangesByEndpoint.Builder(); + b.put(endpoint, Replica.fullReplica(endpoint, range)); + return b.build(); + } + + public static RangesByEndpoint transientReplicas(InetAddressAndPort endpoint, Range range) + { + RangesByEndpoint.Builder b = new RangesByEndpoint.Builder(); + b.put(endpoint, Replica.transientReplica(endpoint, range)); + return b.build(); + } + + public static PlacementDeltas deltas(DataPlacements first, DataPlacements second) + { + assert first.asMap().keySet().equals(second.asMap().keySet()); + + PlacementDeltas.Builder deltas = PlacementDeltas.builder(first.size()); + first.asMap().forEach((params, placement) -> { + deltas.put(params, placement.difference(second.get(params))); + }); + return deltas.build(); + } + + public static DataPlacements placements(List> ranges, + Set replication, + Random random) + { + DataPlacements.Builder allPlacements = DataPlacements.builder(replication.size()); + replication.forEach((params) -> { + assertSame("Only simple replication params are necessary/permitted here", SimpleStrategy.class, params.klass); + String s = params.options.get("replication_factor"); + assertNotNull(s); + int rf = Integer.parseInt(s); + DataPlacement.Builder placement = DataPlacement.builder(); + for (Range range : ranges) + { + // pick rf random nodes to be replicas, no duplicates + Set replicas = new HashSet<>(rf); + while (replicas.size() < rf) + replicas.add(randomEndpoint(random)); + + replicas.forEach(e -> { + Replica replica = Replica.fullReplica(e, range); + placement.withReadReplica(Epoch.FIRST, replica).withWriteReplica(Epoch.FIRST, replica); + }); + } + allPlacements.with(params, placement.build()); + }); + return allPlacements.build(); + } + + public static List> ranges(Random random) + { + // min 10, max 40 ranges + int count = random.nextInt(30) + 10; + return ranges(count, Murmur3Partitioner.instance, random); + } + + public static List> ranges(int count, IPartitioner partitioner, Random random) + { + Set tokens = new HashSet<>(count); + while(tokens.size() < count-1) + tokens.add(partitioner.getRandomToken(random)); + return ranges(tokens, partitioner); + } + + public static List> ranges(Collection tokens, IPartitioner partitioner) + { + List sorted = new ArrayList<>(tokens); + Collections.sort(sorted); + + List> ranges = new ArrayList<>(tokens.size() + 1); + ranges.add(new Range<>(partitioner.getMinimumToken(), sorted.get(0))); + for (int i = 1; i < sorted.size(); i++) + ranges.add(new Range<>(sorted.get(i-1), sorted.get(i))); + ranges.add(new Range<>(sorted.get(sorted.size() - 1), partitioner.getMinimumToken())); + return ranges; + } + + public static DataPlacements randomPlacements(Random random) + { + Set replication = ImmutableSet.of(KeyspaceParams.simple(1).replication, + KeyspaceParams.simple(2).replication, + KeyspaceParams.simple(3).replication); + return placements(ranges(random), replication, random); + } + + public static void setLocalTokens(int... tokens) + { + Set joiningTokens = new HashSet<>(); + for (int token : tokens) + joiningTokens.add(token(token)); + ClusterMetadataTestHelper.join(broadcastAddress, joiningTokens); + } + + + public static RangesAtEndpoint generateRangesAtEndpoint(InetAddressAndPort endpoint, int... rangePairs) + { + if (rangePairs.length % 2 == 1) + throw new RuntimeException("generateRangesAtEndpoint argument count should be even"); + + RangesAtEndpoint.Builder builder = RangesAtEndpoint.builder(endpoint); + + for (int i = 0; i < rangePairs.length; i += 2) + { + builder.add(Replica.fullReplica(endpoint, generateRange(rangePairs[i], rangePairs[i + 1]))); + } + return builder.build(); + } + + public static List> generateRanges(int... rangePairs) + { + if (rangePairs.length % 2 == 1) + throw new RuntimeException("generateRanges argument count should be even"); + + List> ranges = new ArrayList<>(); + + for (int i = 0; i < rangePairs.length; i += 2) + { + ranges.add(generateRange(rangePairs[i], rangePairs[i + 1])); + } + + return ranges; + } + + public static Range generateRange(int left, int right) + { + return new Range<>(token(left), token(right)); + } + + public static Token token(int token) + { + return new Murmur3Partitioner.LongToken(token); + } + + public static Token bytesToken(int token) + { + return new ByteOrderedPartitioner.BytesToken(ByteBufferUtil.bytes(token)); + } + public static void beginJoin(int... tokens) + { + Set newTokens = new HashSet<>(); + for (int token : tokens) + newTokens.add(token(token)); + ClusterMetadataTestHelper.joinPartially(broadcastAddress, newTokens); + } + + public static void beginMove(int... tokens) + { + Set newTokens = new HashSet<>(); + for (int token : tokens) + newTokens.add(token(token)); + ClusterMetadataTestHelper.movePartially(broadcastAddress, newTokens); + } + + public static PlacementDeltas randomDeltas(List> ranges, Random random) + { + Set replication = ImmutableSet.of(KeyspaceParams.simple(1).replication, + KeyspaceParams.simple(2).replication, + KeyspaceParams.simple(3).replication); + return deltas(placements(ranges, replication, random), placements(ranges, replication, random)); + } + + public static PlacementDeltas randomDeltas(IPartitioner partitioner, Random random) + { + List> ranges = ranges(10, partitioner, random); + return randomDeltas(ranges,random); + } + + public static Set randomTokens(int numTokens, IPartitioner partitioner, Random random) + { + Set tokens = new HashSet<>(numTokens); + while(tokens.size() < numTokens) + tokens.add(partitioner.getRandomToken(random)); + return tokens; + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ownership/PrimaryRangeComparatorTest.java b/test/unit/org/apache/cassandra/tcm/ownership/PrimaryRangeComparatorTest.java new file mode 100644 index 0000000000..3f2b411a7f --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ownership/PrimaryRangeComparatorTest.java @@ -0,0 +1,107 @@ +/* + * 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.tcm.ownership; + +import java.util.Collections; + +import org.junit.Test; + +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; + +import static org.apache.cassandra.locator.Replica.fullReplica; +import static org.apache.cassandra.tcm.membership.MembershipUtils.endpoint; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.junit.Assert.assertEquals; + +public class PrimaryRangeComparatorTest +{ + @Test + public void tokenOwnerSortsFirst() + { + Location location = new Location("dc1", "rack1"); + Directory directory = new Directory(); + TokenMap tokenMap = new TokenMap(Murmur3Partitioner.instance); + + InetAddressAndPort ep1 = endpoint(1); + directory = directory.with(new NodeAddresses(endpoint(1)), location); + NodeId id1 = directory.peerId(ep1); + tokenMap = tokenMap.assignTokens(id1, Collections.singleton(token(100))); + + InetAddressAndPort ep2 = endpoint(2); + directory = directory.with(new NodeAddresses(endpoint(2)), location); + NodeId id2 = directory.peerId(ep2); + tokenMap = tokenMap.assignTokens(id2, Collections.singleton(token(200))); + + InetAddressAndPort ep3 = endpoint(3); + directory = directory.with(new NodeAddresses(endpoint(3)), location); + NodeId id3 = directory.peerId(ep3); + tokenMap = tokenMap.assignTokens(id3, Collections.singleton(token(300))); + + Range range = new Range<>(token(100), token(200)); + EndpointsForRange replicas = EndpointsForRange.builder(range) + .add(fullReplica(ep1, range)) + .add(fullReplica(ep2, range)) + .add(fullReplica(ep3, range)) + .build(); + PrimaryRangeComparator c = new PrimaryRangeComparator(tokenMap, directory); + EndpointsForRange sorted = replicas.sorted(c); + assertEquals(ep2, sorted.iterator().next().endpoint()); + } + + @Test + public void whenWraparoundLowestTokenOwnerSortsFirst() + { + Location location = new Location("dc1", "rack1"); + Directory directory = new Directory(); + TokenMap tokenMap = new TokenMap(Murmur3Partitioner.instance); + + InetAddressAndPort ep1 = endpoint(1); + directory = directory.with(new NodeAddresses(endpoint(1)), location); + NodeId id1 = directory.peerId(ep1); + tokenMap = tokenMap.assignTokens(id1, Collections.singleton(token(100))); + + InetAddressAndPort ep2 = endpoint(2); + directory = directory.with(new NodeAddresses(endpoint(2)), location); + NodeId id2 = directory.peerId(ep2); + tokenMap = tokenMap.assignTokens(id2, Collections.singleton(token(200))); + + InetAddressAndPort ep3 = endpoint(3); + directory = directory.with(new NodeAddresses(endpoint(3)), location); + NodeId id3 = directory.peerId(ep3); + tokenMap = tokenMap.assignTokens(id3, Collections.singleton(token(300))); + + Range range = new Range<>(token(300), Murmur3Partitioner.MINIMUM); + EndpointsForRange replicas = EndpointsForRange.builder(range) + .add(fullReplica(ep1, range)) + .add(fullReplica(ep2, range)) + .add(fullReplica(ep3, range)) + .build(); + PrimaryRangeComparator c = new PrimaryRangeComparator(tokenMap, directory); + EndpointsForRange sorted = replicas.sorted(c); + assertEquals(ep1, sorted.iterator().next().endpoint()); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java new file mode 100644 index 0000000000..bf6b84fdd7 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java @@ -0,0 +1,100 @@ +/* + * 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.tcm.ownership; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.distributed.test.log.CMSTestBase; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.distributed.test.log.MetadataChangeSimulationTest; +import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; + +public class UniformRangePlacementIntegrationTest +{ + private static final PlacementSimulator.ReplicationFactor RF = new PlacementSimulator.NtsReplicationFactor(3, 3); + private CMSTestBase.CMSSut sut; + @BeforeClass + public static void beforeClass() + { + ServerTestUtils.prepareServerNoRegister(); + } + + @Before + public void before() throws ExecutionException, InterruptedException + { + ClusterMetadataService.unsetInstance(); + sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, RF); + } + + @After + public void after() throws Exception + { + sut.close(); + } + + @Test + public void testMultiDC() throws Throwable + { + UniformRangePlacement rangePlacement = new UniformRangePlacement(); + Random rng = new Random(1); + int idx = 1; + PlacementSimulator.NodeFactory factory = PlacementSimulator.nodeFactory(); + List nodes = new ArrayList<>(); + for (int i = 0; i < 5; i++) + { + for (int j = 1; j <= 3; j++) + { + int dc = j; + int rack = (rng.nextInt(3) + 1); + PlacementSimulator.Node node = factory.make(idx, dc, rack); + ClusterMetadataTestHelper.register(idx, node.dc(), node.rack()); + ClusterMetadataTestHelper.join(idx, node.token()); + nodes.add(node); + idx++; + } + } + nodes.sort(PlacementSimulator.Node::compareTo); + + ClusterMetadataService.instance().processor().fetchLogAndWait(); + DataPlacements placements = rangePlacement.calculatePlacements(ClusterMetadata.current().epoch, + ClusterMetadata.current(), + Keyspaces.of(ClusterMetadata.current().schema.getKeyspaces().get("test").get())); + + PlacementSimulator.ReplicatedRanges predicted = RF.replicate(nodes); + + ReplicationParams replicationParams = ClusterMetadata.current().schema.getKeyspaces().get("test").get().params.replication; + MetadataChangeSimulationTest.match(placements.get(replicationParams).reads, + predicted.asMap()); + } +} + diff --git a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementTest.java b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementTest.java new file mode 100644 index 0000000000..f046be7955 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementTest.java @@ -0,0 +1,320 @@ +/* + * 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.tcm.ownership; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import org.junit.Test; + +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.tcm.Epoch; + +import static org.apache.cassandra.tcm.membership.MembershipUtils.endpoint; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class UniformRangePlacementTest +{ + + @Test + public void testSplittingPlacementWithSingleRange() + { + // Special case of splitting a range that was created because the initial token list contained a single token, + // which is either the min or max value in the token space. Any other single token would produce two ranges - + // (MIN, t] & (t, MAX] - but because (x, x] denotes a wraparound, this case produces (MIN, MAX] and we need to + // verify that we can safely split that when more tokens are introduced. This test supposes MIN = 0, MAX = 100 + PlacementForRange before = PlacementForRange.builder() + .withReplicaGroup(VersionedEndpoints.forRange(Epoch.EMPTY, rg(0, 100, 1, 2, 3))) + .build(); + // existing token is MIN (i.e. 0 for the purposes of this test) + List tokens = ImmutableList.of(token(0), token(30), token(60), token(90)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 30, 1, 2, 3), + rg(30, 60, 1, 2, 3), + rg(60, 90, 1, 2, 3), + rg(90, 100, 1, 2, 3)); + + + // existing token is MAX (i.e. 100 for the purposes of this test). + tokens = ImmutableList.of(token(30), token(60), token(90), token(100)); + after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 30, 1, 2, 3), + rg(30, 60, 1, 2, 3), + rg(60, 90, 1, 2, 3), + rg(90, 100, 1, 2, 3)); + } + + @Test + public void testSplitSingleRange() + { + // Start with: (0,100] : (n1,n2,n3); + // (100,200] : (n1,n2,n3); + // (200,300] : (n1,n2,n3); + // (300,400] : (n1,n2,n3); + PlacementForRange before = initialPlacement(); + // split (100,200] to (100,150], (150,200] + List tokens = ImmutableList.of(token(100), token(150), token(200), token(300)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 100, 1, 2, 3), + rg(100, 150, 1, 2, 3), + rg(150, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3)); + } + + @Test + public void testSplitMultipleDisjointRanges() + { + // Start with: (0,100] : (n1,n2,n3); + // (100,200] : (n1,n2,n3); + // (200,300] : (n1,n2,n3); + // (300,400] : (n1,n2,n3); + PlacementForRange before = initialPlacement(); + // split (100,200] to (100,150],(150,200] + // and (200,300] to (200,250],(250,300] + List tokens = ImmutableList.of(token(100), token(150), token(200), token(250), token(300)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 100, 1, 2, 3), + rg(100, 150, 1, 2, 3), + rg(150, 200, 1, 2, 3), + rg(200, 250, 1, 2, 3), + rg(250, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3)); + } + + @Test + public void testSplitSingleRangeMultipleTimes() + { + // Start with: (0,100] : (n1,n2,n3); + // (100,200] : (n1,n2,n3); + // (200,300] : (n1,n2,n3); + // (300,400] : (n1,n2,n3); + PlacementForRange before = initialPlacement(); + // split (100,200] to (100,125],(125,150],(150,200] + List tokens = ImmutableList.of(token(100), token(125), token(150), token(200), token(300)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 100, 1, 2, 3), + rg(100, 125, 1, 2, 3), + rg(125, 150, 1, 2, 3), + rg(150, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3)); + } + + @Test + public void testSplitMultipleRangesMultipleTimes() + { + PlacementForRange before = initialPlacement(); + // split (100,200] to (100,125],(125,150],(150,200] + // and (200,300] to (200,225],(225,250],(250,300] + List tokens = ImmutableList.of(token(100), token(125), token(150), token(200), token(225), token(250), token(300)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 100, 1, 2, 3), + rg(100, 125, 1, 2, 3), + rg(125, 150, 1, 2, 3), + rg(150, 200, 1, 2, 3), + rg(200, 225, 1, 2, 3), + rg(225, 250, 1, 2, 3), + rg(250, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3)); + } + + @Test + public void testSplitLastRangeMultipleTimes() + { + // Start with: (0,100] : (n1,n2,n3); + // (100,200] : (n1,n2,n3); + // (200,300] : (n1,n2,n3); + // (300,400] : (n1,n2,n3); + PlacementForRange before = initialPlacement(); + // split (300,400] to (300,325],(325,350],(350,400] + List tokens = ImmutableList.of(token(100), token(200), token(300), token(325), token(350)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 100, 1, 2, 3), + rg(100, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 325, 1, 2, 3), + rg(325, 350, 1, 2, 3), + rg(350, 400, 1, 2, 3)); + } + + @Test + public void testSplitFirstRangeMultipleTimes() + { + // Start with: (0,100] : (n1,n2,n3); + // (100,200] : (n1,n2,n3); + // (200,300] : (n1,n2,n3); + // (300,400] : (n1,n2,n3); + PlacementForRange before = initialPlacement(); + // split (0,100] to (0,25],(25,50],(50,100] + List tokens = ImmutableList.of(token(25), token(50), token(100), token(200), token(300)); + PlacementForRange after = PlacementForRange.splitRangesForPlacement(tokens, before); + assertPlacement(after, + rg(0, 25, 1, 2, 3), + rg(25, 50, 1, 2, 3), + rg(50, 100, 1, 2, 3), + rg(100, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3)); + } + + @Test + public void testCombiningPlacements() + { + EndpointsForRange[] firstGroups = { rg(0, 100, 1, 2, 3), + rg(100, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3) }; + PlacementForRange p1 = PlacementForRange.builder() + .withReplicaGroups(Arrays.asList(firstGroups).stream().map(this::v).collect(Collectors.toList())) + .build(); + EndpointsForRange[] secondGroups = { rg(0, 100, 2, 3, 4), + rg(100, 200, 2, 3, 5), + rg(200, 300, 2, 3, 6), + rg(300, 400, 2, 3, 7) }; + PlacementForRange p2 = PlacementForRange.builder() + .withReplicaGroups(Arrays.asList(secondGroups).stream().map(this::v).collect(Collectors.toList())) + .build(); + + ReplicationParams params = ReplicationParams.simple(1); + DataPlacements map1 = DataPlacements.builder(1).with(params, new DataPlacement(p1, p1)).build(); + DataPlacements map2 = DataPlacements.builder(1).with(params, new DataPlacement(p2, p2)).build(); + DataPlacement p3 = map1.combineReplicaGroups(map2).get(params); + for (PlacementForRange placement : new PlacementForRange[]{ p3.reads, p3.writes }) + { + assertPlacement(placement, + rg(0, 100, 1, 2, 3, 4), + rg(100, 200, 1, 2, 3, 5), + rg(200, 300, 1, 2, 3, 6), + rg(300, 400, 1, 2, 3, 7)); + } + } + + @Test + public void testSplittingNeedsSorting() + { + List tokens = ImmutableList.of(token(-4611686018427387905L), token(-3L)); + EndpointsForRange[] initial = { rg(-9223372036854775808L, -4611686018427387905L, 1), + rg(-4611686018427387905L, -9223372036854775808L, 1)}; + DataPlacement.Builder builder = DataPlacement.builder(); + builder.writes.withReplicaGroups(Arrays.asList(initial).stream().map(this::v).collect(Collectors.toList())); + + DataPlacement initialPlacement = builder.build(); + DataPlacement split = initialPlacement.splitRangesForPlacement(tokens); + assertPlacement(split.writes, rg(-3, -9223372036854775808L, 1), + rg(-9223372036854775808L,-4611686018427387905L, 1), + rg(-4611686018427387905L, -3, 1)); + } + + @Test + public void testInitialFullWrappingRange() + { + /* + TOKENS=[-9223372036854775808, 3074457345618258602] PLACEMENT=[[Full(/127.0.0.1:7000,(-9223372036854775808,-9223372036854775808])]] + */ + List tokens = ImmutableList.of(token(-9223372036854775808L), token(3074457345618258602L)); + EndpointsForRange[] initial = { rg(-9223372036854775808L, -9223372036854775808L, 1)}; + + DataPlacement.Builder builder = DataPlacement.builder(); + builder.writes.withReplicaGroups(Arrays.asList(initial).stream().map(this::v).collect(Collectors.toList())); + + DataPlacement initialPlacement = builder.build(); + DataPlacement split = initialPlacement.splitRangesForPlacement(tokens); + assertPlacement(split.writes, rg(3074457345618258602L,-9223372036854775808L, 1), rg(-9223372036854775808L, 3074457345618258602L, 1)); + } + + @Test + public void testWithMinToken() + { + EndpointsForRange initialRG = rg(Long.MIN_VALUE, Long.MIN_VALUE, 1, 2, 3); + + DataPlacement.Builder builder = DataPlacement.builder(); + builder.writes.withReplicaGroup(v(initialRG)); + + DataPlacement initialPlacement = builder.build(); + List tokens = ImmutableList.of(token(Long.MIN_VALUE), token(0)); + DataPlacement newPlacement = initialPlacement.splitRangesForPlacement(tokens); + assertEquals(2, newPlacement.writes.replicaGroups.values().size()); + } + + private PlacementForRange initialPlacement() + { + EndpointsForRange[] initialGroups = { rg(0, 100, 1, 2, 3), + rg(100, 200, 1, 2, 3), + rg(200, 300, 1, 2, 3), + rg(300, 400, 1, 2, 3) }; + PlacementForRange placement = PlacementForRange.builder() + .withReplicaGroups(Arrays.asList(initialGroups).stream().map(this::v).collect(Collectors.toList())) + .build(); + assertPlacement(placement, initialGroups); + return placement; + } + + private void assertPlacement(PlacementForRange placement, EndpointsForRange...expected) + { + Collection replicaGroups = placement.replicaGroups.values().stream().map(v -> v.get()).collect(Collectors.toList()); + assertEquals(replicaGroups.size(), expected.length); + int i = 0; + boolean allMatch = true; + for(EndpointsForRange group : replicaGroups) + if (!Iterables.elementsEqual(group, expected[i++])) + allMatch = false; + + assertTrue(String.format("Placement didn't match expected replica groups. " + + "%nExpected: %s%nActual: %s", Arrays.asList(expected), replicaGroups), + allMatch); + } + + private VersionedEndpoints.ForRange v(EndpointsForRange rg) + { + return VersionedEndpoints.forRange(Epoch.FIRST, rg); + } + + private VersionedEndpoints.ForToken v(EndpointsForToken rg) + { + return VersionedEndpoints.forToken(Epoch.FIRST, rg); + } + + private EndpointsForRange rg(long t0, long t1, int...replicas) + { + Range range = new Range<>(token(t0), token(t1)); + EndpointsForRange.Builder builder = EndpointsForRange.builder(range); + for (int i : replicas) + builder.add(Replica.fullReplica(endpoint((byte)i), range)); + return builder.build(); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/sequences/InProgressSequenceCancellationTest.java b/test/unit/org/apache/cassandra/tcm/sequences/InProgressSequenceCancellationTest.java new file mode 100644 index 0000000000..44b04abcf2 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/sequences/InProgressSequenceCancellationTest.java @@ -0,0 +1,344 @@ +/* + * 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.tcm.sequences; + +import java.util.Collections; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.EndpointsForRange; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.ownership.DataPlacement; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementForRange; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareReplace; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.ReplicationParams; + +import static org.apache.cassandra.tcm.membership.MembershipUtils.nodeAddresses; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.deltas; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.placements; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.ranges; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.token; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.affectedRanges; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.epoch; +import static org.apache.cassandra.tcm.sequences.SequencesUtils.lockedRanges; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class InProgressSequenceCancellationTest +{ + + private static final Logger logger = LoggerFactory.getLogger(InProgressSequenceCancellationTest.class); + private static final int ITERATIONS = 100; + + @BeforeClass + public static void setup() + { + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + // disable the sorting of replica lists as it assumes all endpoints are present in + // the token map and this test uses randomly generated placements, so this is not true + CassandraRelevantProperties.TCM_SORT_REPLICA_GROUPS.setBoolean(false); + } + + @Test + public void revertBootstrapAndJoinEffects() throws Throwable + { + for (int i = 0; i < ITERATIONS; i++) + testRevertingBootstrap(System.nanoTime()); + } + + private void testRevertingBootstrap(long seed) + { + Random random = new Random(seed); + logger.info("SEED: {}", seed); + Set replication = ImmutableSet.of(KeyspaceParams.simple(1).replication, + KeyspaceParams.simple(2).replication, + KeyspaceParams.simple(3).replication, + KeyspaceParams.simple(5).replication, + KeyspaceParams.simple(10).replication); + NodeAddresses addresses = nodeAddresses(random); + Directory directory = new Directory().with(addresses, new Location("dc", "rack")); + NodeId nodeId = directory.peerId(addresses.broadcastAddress); + LockedRanges.Key key = LockedRanges.keyFor(epoch(random)); + // Initial placements, i.e. state before the sequence was initiated - what we want to return to + DataPlacements placements = placements(ranges(random), replication, random); + // Ranges locked by other operations + LockedRanges locked = lockedRanges(placements, random); + + // state of metadata before starting the sequence + ClusterMetadata before = metadata(directory).transformer() + .with(placements) + .withNodeState(nodeId, NodeState.REGISTERED) + .with(locked) + .build().metadata; + + // Placements after PREPARE_JOIN + DataPlacements afterPrepare = placements(ranges(random), replication, random); + PlacementDeltas prepareDeltas = deltas(placements, afterPrepare); + // Placements after START_JOIN + DataPlacements afterStart = placements(ranges(random), replication, random); + PlacementDeltas startDeltas = deltas(afterPrepare, afterStart); + // Placements after MID_JOIN + DataPlacements afterMid = placements(ranges(random), replication, random); + PlacementDeltas midDeltas = deltas(afterStart, afterMid); + // No need to create a deltas or placements for after FINISH_JOIN because it's too late to cancel by then + PlacementDeltas finishDeltas = PlacementDeltas.empty(); + + Set tokens = Collections.singleton(token(random.nextLong())); + + BootstrapAndJoin plan = new BootstrapAndJoin(Epoch.EMPTY, + key, + prepareDeltas, + Transformation.Kind.FINISH_JOIN, + new PrepareJoin.StartJoin(nodeId, startDeltas, key), + new PrepareJoin.MidJoin(nodeId, midDeltas, key), + new PrepareJoin.FinishJoin(nodeId, tokens, finishDeltas, key), + false, + false); + + // Ranges locked by this sequence + locked = locked.lock(key, affectedRanges(placements, random)); + // State of metadata after executing up to the FINISH step + ClusterMetadata during = before.transformer() + .with(afterMid) + .with(locked) + .withNodeState(nodeId, NodeState.BOOTSTRAPPING) + .with(before.inProgressSequences.with(nodeId, plan)) + .build().metadata; + + ClusterMetadata after = plan.cancel(during).build().metadata; + + assertRelevantMetadata(before, after); + // cancelling the sequence doesn't remove it from metadata, that's the job of the CancelInProgressSequence event + assertNull(before.inProgressSequences.get(nodeId)); + assertEquals(plan, after.inProgressSequences.get(nodeId)); + } + + @Test + public void revertUnbootstrapAndLeaveEffects() throws Throwable + { + for (int i = 0; i < ITERATIONS; i++) + testRevertingLeave(System.nanoTime()); + } + + private void testRevertingLeave(long seed) + { + Random random = new Random(seed); + logger.info("SEED: {}", seed); + Set replication = ImmutableSet.of(KeyspaceParams.simple(1).replication, + KeyspaceParams.simple(2).replication, + KeyspaceParams.simple(3).replication, + KeyspaceParams.simple(5).replication, + KeyspaceParams.simple(10).replication); + + NodeAddresses addresses = nodeAddresses(random); + Directory directory = new Directory().with(addresses, new Location("dc", "rack")); + NodeId nodeId = directory.peerId(addresses.broadcastAddress); + LockedRanges.Key key = LockedRanges.keyFor(epoch(random)); + // Initial placements, i.e. state before the sequence was initiated - what we want to return to + DataPlacements placements = placements(ranges(random), replication, random); + // Ranges locked by other operations + LockedRanges locked = lockedRanges(placements, random); + // state of metadata before starting the sequence + ClusterMetadata before = metadata(directory).transformer() + .with(placements) + .withNodeState(nodeId, NodeState.JOINED) + .with(locked) + .build().metadata; + + + // PREPARE_LEAVE does not modify placements, so first transformation is START_LEAVE + DataPlacements afterStart = placements(ranges(random), replication, random); + PlacementDeltas startDeltas = deltas(placements, afterStart); + // Placements after MID_LEAVE + DataPlacements afterMid = placements(ranges(random), replication, random); + PlacementDeltas midDeltas = deltas(afterStart, afterMid); + + UnbootstrapAndLeave plan = new UnbootstrapAndLeave(Epoch.EMPTY, + key, + Transformation.Kind.FINISH_LEAVE, + new PrepareLeave.StartLeave(nodeId, startDeltas, key), + new PrepareLeave.MidLeave(nodeId, midDeltas, key), + new PrepareLeave.FinishLeave(nodeId, PlacementDeltas.empty(), key), + new UnbootstrapStreams()); + + // Ranges locked by this sequence (just random, not accurate, as we're only asserting that they get unlocked) + locked = locked.lock(key, affectedRanges(placements, random)); + // State of metadata after executing up to the FINISH step + ClusterMetadata during = before.transformer() + .with(afterMid) + .with(locked) + .withNodeState(nodeId, NodeState.LEAVING) + .with(before.inProgressSequences.with(nodeId, plan)) + .build().metadata; + + ClusterMetadata after = plan.cancel(during).build().metadata; + + assertRelevantMetadata(before, after); + // cancelling the sequence doesn't remove it from metadata, that's the job of the CancelInProgressSequence event + assertNull(before.inProgressSequences.get(nodeId)); + assertEquals(plan, after.inProgressSequences.get(nodeId)); + } + + @Test + public void revertBootstrapAndReplaceEffects() throws Throwable + { + for (int i = 0; i < ITERATIONS; i++) + testRevertingReplace(System.nanoTime()); + } + + private void testRevertingReplace(long seed) + { + Random random = new Random(seed); + logger.info("SEED: {}", seed); + Set replication = ImmutableSet.of(KeyspaceParams.simple(1).replication, + KeyspaceParams.simple(2).replication, + KeyspaceParams.simple(3).replication, + KeyspaceParams.simple(5).replication, + KeyspaceParams.simple(10).replication); + + NodeAddresses addresses = nodeAddresses(random); + Directory directory = new Directory().with(addresses, new Location("dc", "rack")); + NodeId nodeId = directory.peerId(addresses.broadcastAddress); + LockedRanges.Key key = LockedRanges.keyFor(epoch(random)); + // Initial placements, i.e. state before the sequence was initiated - what we want to return to + DataPlacements placements = placements(ranges(random), replication, random); + // Ranges locked by other operations + LockedRanges locked = lockedRanges(placements, random); + // State of metadata before starting the sequence + ClusterMetadata before = metadata(directory).transformer() + .with(placements) + .withNodeState(nodeId, NodeState.REGISTERED) + .with(locked) + .build().metadata; + + // nodeId is the id of the replacement node. Add the node being replaced to metadata + NodeAddresses replacedAddresses = nodeAddresses(random); + // Make sure we don't try to replace with the same address + while (replacedAddresses.broadcastAddress.equals(addresses.broadcastAddress)) + replacedAddresses = nodeAddresses(random); + before = before.transformer() + .register(replacedAddresses, new Location("dc", "rack"), NodeVersion.CURRENT) + .build().metadata; + NodeId replacedId = before.directory.peerId(replacedAddresses.broadcastAddress); + Set tokens = Collections.singleton(token(random.nextLong())); + // bit of a hack to jump the old node directly to joined + before = before.transformer().proposeToken(replacedId, tokens).build().metadata; + before = before.transformer().join(replacedId).build().metadata; + + // PREPARE_REPLACE does not modify placements, so first transformation is START_REPLACE + DataPlacements afterStart = placements(ranges(random), replication, random); + PlacementDeltas startDeltas = deltas(placements, afterStart); + // Placements after MID_REPLACE + DataPlacements afterMid = placements(ranges(random), replication, random); + PlacementDeltas midDeltas = deltas(afterStart, afterMid); + + BootstrapAndReplace plan = new BootstrapAndReplace(Epoch.EMPTY, + key, + tokens, + Transformation.Kind.FINISH_REPLACE, + new PrepareReplace.StartReplace(replacedId, nodeId, startDeltas, key), + new PrepareReplace.MidReplace(replacedId, nodeId, midDeltas, key), + new PrepareReplace.FinishReplace(replacedId, nodeId, PlacementDeltas.empty(), key), + false, + false); + + // Ranges locked by this sequence (just random, not accurate, as we're only asserting that they get unlocked) + locked = locked.lock(key, affectedRanges(placements, random)); + // State of metadata after executing up to the FINISH step + ClusterMetadata during = before.transformer() + .with(afterMid) + .with(locked) + .with(before.inProgressSequences.with(nodeId, plan)) + .build().metadata; + + ClusterMetadata after = plan.cancel(during).build().metadata; + + assertRelevantMetadata(before, after); + // cancelling the sequence doesn't remove it from metadata, that's the job of the CancelInProgressSequence event + assertNull(before.inProgressSequences.get(nodeId)); + assertEquals(plan, after.inProgressSequences.get(nodeId)); + } + + private void assertRelevantMetadata(ClusterMetadata first, ClusterMetadata second) + { + assertPlacementsEquivalent(first.placements, second.placements); + assertTrue(first.directory.isEquivalent(second.directory)); + assertTrue(first.tokenMap.isEquivalent(second.tokenMap)); + assertEquals(first.lockedRanges.locked.keySet(), second.lockedRanges.locked.keySet()); + } + + private static ClusterMetadata metadata(Directory directory) + { + return new ClusterMetadata(Murmur3Partitioner.instance, directory); + } + + private void assertPlacementsEquivalent(DataPlacements first, DataPlacements second) + { + assertEquals(first.keys(), second.keys()); + + first.asMap().forEach((params, placement) -> { + DataPlacement otherPlacement = second.get(params); + PlacementForRange r1 = placement.reads; + PlacementForRange r2 = otherPlacement.reads; + assertEquals(r1.replicaGroups().keySet(), r2.replicaGroups().keySet()); + r1.replicaGroups().forEach((range, e1) -> { + EndpointsForRange e2 = r2.forRange(range).get(); + assertEquals(e1.size(),e2.size()); + assertTrue(e1.get().stream().allMatch(e2::contains)); + }); + + PlacementForRange w1 = placement.reads; + PlacementForRange w2 = otherPlacement.reads; + assertEquals(w1.replicaGroups().keySet(), w2.replicaGroups().keySet()); + w1.replicaGroups().forEach((range, e1) -> { + EndpointsForRange e2 = w2.forRange(range).get(); + assertEquals(e1.size(),e2.size()); + assertTrue(e1.get().stream().allMatch(e2::contains)); + }); + + }); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java new file mode 100644 index 0000000000..e5c5ab4145 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java @@ -0,0 +1,351 @@ +/* + * 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.tcm.sequences; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import harry.generators.PCGFastPure; +import harry.generators.PcgRSUFast; +import harry.generators.RandomGenerator; +import harry.generators.Surjections; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IIsolatedExecutor; +import org.apache.cassandra.distributed.test.log.CMSTestBase; +import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.distributed.test.log.RngUtils; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.ConnectionType; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessageDelivery; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.tcm.AtomicLongBackedProcessor; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.MultiStepOperation; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeAddresses; +import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.Register; +import org.apache.cassandra.tcm.transformations.UnsafeJoin; +import org.apache.cassandra.utils.concurrent.Future; + +public class ProgressBarrierTest extends CMSTestBase +{ + static + { + DatabaseDescriptor.setRpcTimeout(10); + DatabaseDescriptor.setProgressBarrierTimeout(1000); + DatabaseDescriptor.setProgressBarrierBackoff(5); + } + + @Test + public void testProgressBarrier() throws Throwable + { + RandomGenerator rng = new PcgRSUFast(1L, 1l); + Supplier respond = bools().toGenerator().bind(rng); + Supplier rfs = combine(ints(0, 3), + ints(1, 5), + bools(), + ints(1, 5), + (Integer dcs, Integer nodesPerDc, Boolean addAlternate, Integer nodesPerDcAlt) -> { + if (dcs == 0) + return new PlacementSimulator.SimpleReplicationFactor(nodesPerDc); + else if (addAlternate && nodesPerDcAlt.intValue() != nodesPerDc.intValue()) + { + int[] perDc = new int[dcs + 1]; + Arrays.fill(perDc, nodesPerDc); + perDc[perDc.length - 1] = nodesPerDcAlt; + return new PlacementSimulator.NtsReplicationFactor(perDc); + } + else + { + return new PlacementSimulator.NtsReplicationFactor(dcs, nodesPerDc); + } + }) + .toGenerator() + .bind(rng); + Supplier nodes = ints(15, 20).toGenerator().bind(rng); + Supplier cls = Surjections.pick( + ConsistencyLevel.ALL, + ConsistencyLevel.QUORUM, + ConsistencyLevel.LOCAL_QUORUM, + ConsistencyLevel.EACH_QUORUM, + ConsistencyLevel.ONE).toGenerator() + .bind(rng); + + PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + for (int run = 0; run < 100; run++) + { + PlacementSimulator.ReplicationFactor rf = rfs.get(); + try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + List allNodes = new ArrayList<>(); + PlacementSimulator.Node node = null; + int nodesInCluster = Math.max(rf.total(), nodes.get()); + for (int i = 1; i <= nodesInCluster; i++) + { + node = nodeFactory.make(i, (i % rf.dcs()) + 1, 1); + allNodes.add(node); + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + if (i < nodesInCluster) + sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), ClusterMetadataService.instance().placementProvider())); + } + + sut.service.commit(new PrepareJoin(node.nodeId(), Collections.singleton(node.longToken()), ClusterMetadataService.instance().placementProvider(), true, false)); + + for (int check = 0; check < 10; check++) + { + ClusterMetadata metadata = ClusterMetadata.current(); + ConsistencyLevel cl = cls.get(); + + Set responded = new ConcurrentSkipListSet<>(); + MessageDelivery delivery = new MessageDelivery() + { + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) + { + // assert that it is a replcia + if (respond.get()) + { + responded.add(to); + cb.onResponse((Message) message.responseWith(message.epoch())); + } + else + { + cb.onFailure(message.from(), RequestFailureReason.TIMEOUT); + } + } + + public void send(Message message, InetAddressAndPort to) {} + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) {} + public Future> sendWithResult(Message message, InetAddressAndPort to) { return null; } + public void respond(V response, Message message) {} + }; + ProgressBarrier progressBarrier = ((MultiStepOperation)metadata.inProgressSequences.get(node.nodeId())) + .advance(metadata.epoch) + .barrier() + .withMessagingService(delivery); + + progressBarrier.await(cl, metadata); + + String dc = metadata.directory.location(node.nodeId()).datacenter; + switch (cl) + { + case ALL: + { + Set replicas = metadata.lockedRanges.locked.get(LockedRanges.keyFor(metadata.epoch)) + .toPeers(rf.asKeyspaceParams().replication, metadata.placements, metadata.directory) + .stream() + .map(n -> metadata.directory.getNodeAddresses(n).broadcastAddress) + .collect(Collectors.toSet()); + + Set collected = responded.stream().filter(replicas::contains).collect(Collectors.toSet()); + int expected = rf.total(); + Assert.assertTrue(String.format("Should have collected at least %d nodes but got %d." + + "\nRF: %s" + + "\nReplicas: %s" + + "\nNodes: %s", expected, collected.size(), rf, replicas, collected), + collected.size() >= expected); + + break; + } + case QUORUM: + { + Set replicas = metadata.lockedRanges.locked.get(LockedRanges.keyFor(metadata.epoch)) + .toPeers(rf.asKeyspaceParams().replication, metadata.placements, metadata.directory) + .stream() + .map(n -> metadata.directory.getNodeAddresses(n).broadcastAddress) + .collect(Collectors.toSet()); + + Set collected = responded.stream().filter(replicas::contains).collect(Collectors.toSet()); + int expected = rf.total() / 2 + 1; + Assert.assertTrue(String.format("Should have collected at least %d nodes but got %d." + + "\nRF: %s" + + "\nReplicas: %s" + + "\nNodes: %s", expected, collected.size(), rf, replicas, collected), + collected.size() >= expected); + + break; + } + case LOCAL_QUORUM: + { + List replicas = new ArrayList<>(metadata.lockedRanges.locked.get(LockedRanges.keyFor(metadata.epoch)) + .toPeers(rf.asKeyspaceParams().replication, metadata.placements, metadata.directory) + .stream() + .filter((n) -> metadata.directory.location(n).datacenter.equals(dc)) + .map(n -> metadata.directory.getNodeAddresses(n).broadcastAddress) + .collect(Collectors.toSet())); + replicas.sort(InetAddressAndPort::compareTo); + Set collected = responded.stream().filter(replicas::contains).collect(Collectors.toSet()); + int expected; + if (rf instanceof PlacementSimulator.SimpleReplicationFactor) + expected = rf.total() / 2 + 1; + else + expected = rf.asMap().get(dc) / 2 + 1; + Assert.assertTrue(String.format("Should have collected at least %d nodes but got %d." + + "\nRF: %s" + + "\nReplicas: %s" + + "\nCollected: %s. Responded: %s", expected, collected.size(), rf, replicas, collected, responded), + collected.size() >= expected); + + break; + } + case EACH_QUORUM: + { + Map byDc = new HashMap<>(); + metadata.lockedRanges.locked.get(LockedRanges.keyFor(metadata.epoch)) + .toPeers(rf.asKeyspaceParams().replication, metadata.placements, metadata.directory) + .forEach(n -> byDc.compute(metadata.directory.location(n).datacenter, + (k, v) -> v == null ? 1 : v + 1)); + + if (rf instanceof PlacementSimulator.SimpleReplicationFactor) + { + int actual = byDc.get(dc); + int expected = rf.asMap().get(dc) / 2 + 1; + Assert.assertTrue(String.format("Shuold have collected at least %d nodes, but got %d." + + "\nRF: %s" + + "\nNodes: %s", expected, byDc.size(), rf, byDc), + actual >= expected); + } + else + { + for (Map.Entry e : byDc.entrySet()) + { + int actual = e.getValue(); + int expected = rf.asMap().get(e.getKey()) / 2 + 1; + Assert.assertTrue(String.format("Shuold have collected at least %d nodes, but got %d." + + "\nRF: %s" + + "\nNodes: %s", expected, byDc.size(), rf, byDc), + actual >= expected); + } + } + break; + } + case ONE: + Set replicas = metadata.lockedRanges.locked.get(LockedRanges.keyFor(metadata.epoch)) + .toPeers(rf.asKeyspaceParams().replication, metadata.placements, metadata.directory) + .stream() + .map(n -> metadata.directory.getNodeAddresses(n).broadcastAddress) + .collect(Collectors.toSet()); + + Assert.assertTrue(String.format("Should have collected at least one of the replicas %s, but got %s." + + "\nRF: %s.\nNodes: %s.", + replicas, responded, rf, allNodes), + responded.stream().anyMatch(replicas::contains)); + break; + } + } + + } + } + } + + @Test + public void testProgressBarrierDegradingConsistency() throws Throwable + { + PlacementSimulator.ReplicationFactor rf = new PlacementSimulator.NtsReplicationFactor(5, 5); + PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + + DatabaseDescriptor.setProgressBarrierMinConsistencyLevel(ConsistencyLevel.ONE); + try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) + { + PlacementSimulator.Node node = null; + for (int i = 1; i <= 4; i++) + { + node = nodeFactory.make(i, (i % rf.dcs()) + 1, 1); + sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT)); + if (i < 4) + sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), ClusterMetadataService.instance().placementProvider())); + } + + sut.service.commit(new PrepareJoin(node.nodeId(), Collections.singleton(node.longToken()), ClusterMetadataService.instance().placementProvider(), true, false)); + + Set responded = new ConcurrentSkipListSet<>(); + MessageDelivery delivery = new MessageDelivery() + { + AtomicInteger counter = new AtomicInteger(); + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb) + { + if (counter.getAndIncrement() == 0) + { + responded.add(to); + cb.onResponse((Message) message.responseWith(message.epoch())); + } + } + + public void send(Message message, InetAddressAndPort to) {} + public void sendWithCallback(Message message, InetAddressAndPort to, RequestCallback cb, ConnectionType specifyConnection) {} + public Future> sendWithResult(Message message, InetAddressAndPort to) { return null; } + public void respond(V response, Message message) {} + }; + + ClusterMetadata metadata = ClusterMetadata.current(); + ProgressBarrier progressBarrier = ((MultiStepOperation) metadata.inProgressSequences.get(node.nodeId())) + .advance(metadata.epoch) + .barrier() + .withMessagingService(delivery); + progressBarrier.await(); + Assert.assertTrue(responded.size() == 1); + } + } + + // TODO: move to a common lib? Is this a good idea? + public static Surjections.Surjection ints(int minInclusive, int maxInclusive) + { + return l -> RngUtils.asInt(l, minInclusive, maxInclusive); + } + + public static Surjections.Surjection longs() + { + return l -> PCGFastPure.next(l, l); + } + + public static Surjections.Surjection bools() + { + return RngUtils::asBoolean; + } + + public static Surjections.Surjection combine(Surjections.Surjection gen1, Surjections.Surjection gen2, + Surjections.Surjection gen3, Surjections.Surjection gen4, + IIsolatedExecutor.QuadFunction res) + { + return (long l) -> { + return res.apply(gen1.inflate(PCGFastPure.next(l, 1)), + gen2.inflate(PCGFastPure.next(l, 2)), + gen3.inflate(PCGFastPure.next(l, 2)), + gen4.inflate(PCGFastPure.next(l, 2))); + }; + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/tcm/sequences/SequencesUtils.java b/test/unit/org/apache/cassandra/tcm/sequences/SequencesUtils.java new file mode 100644 index 0000000000..9ffac10c52 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/sequences/SequencesUtils.java @@ -0,0 +1,172 @@ +/* + * 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.tcm.sequences; + +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.Predicate; + +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.Transformation; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.transformations.PrepareJoin; +import org.apache.cassandra.tcm.transformations.PrepareLeave; +import org.apache.cassandra.tcm.transformations.PrepareMove; +import org.apache.cassandra.tcm.transformations.PrepareReplace; + +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_JOIN; +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_LEAVE; +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_MOVE; +import static org.apache.cassandra.tcm.Transformation.Kind.FINISH_REPLACE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_JOIN; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_LEAVE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_MOVE; +import static org.apache.cassandra.tcm.Transformation.Kind.MID_REPLACE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_JOIN; +import static org.apache.cassandra.tcm.Transformation.Kind.START_LEAVE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_MOVE; +import static org.apache.cassandra.tcm.Transformation.Kind.START_REPLACE; +import static org.apache.cassandra.tcm.membership.MembershipUtils.node; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomDeltas; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.randomTokens; +import static org.apache.cassandra.tcm.ownership.OwnershipUtils.ranges; + +public class SequencesUtils +{ + public static LockedRanges lockedRanges(DataPlacements placements, Random random) + { + LockedRanges locked = LockedRanges.EMPTY; + for (int i = 0; i < random.nextInt(10) + 1; i++) + locked = locked.lock(LockedRanges.keyFor(epoch(random)), affectedRanges(placements, random)); + return locked; + } + + public static LockedRanges.AffectedRanges affectedRanges(DataPlacements placements, Random random) + { + LockedRanges.AffectedRangesBuilder affected = LockedRanges.AffectedRanges.builder(); + placements.asMap().forEach((params, placement) -> { + placement.reads.replicaGroups().keySet().forEach((range) -> { + if (random.nextDouble() >= 0.6) + affected.add(params, range); + }); + }); + return affected.build(); + } + + public static Epoch epoch(Random random) + { + return Epoch.create(Math.abs(random.nextLong())); + } + + public static BootstrapAndJoin bootstrapAndJoin(IPartitioner partitioner, Random random, Predicate alreadyInUse) + { + Transformation.Kind[] potentialNextStates = {START_JOIN, MID_JOIN, FINISH_JOIN}; + NodeId node = node(random); + while (alreadyInUse.test(node)) + node = node(random); + Epoch epoch = epoch(random); + Set tokens = randomTokens(10, partitioner, random); + List> ranges = ranges(tokens, partitioner); + PlacementDeltas deltas = randomDeltas(ranges, random); + LockedRanges.Key key = LockedRanges.keyFor(epoch); + return new BootstrapAndJoin(epoch, + key, + deltas, + potentialNextStates[random.nextInt(3)], + new PrepareJoin.StartJoin(node, deltas, key), + new PrepareJoin.MidJoin(node, deltas, key), + new PrepareJoin.FinishJoin(node, tokens, deltas, key), + true, + true); + } + + public static BootstrapAndReplace bootstrapAndReplace(IPartitioner partitioner, Random random, Predicate alreadyInUse) + { + Transformation.Kind[] potentialNextStates = {START_REPLACE, MID_REPLACE, FINISH_REPLACE}; + NodeId replaced = node(random); + NodeId replacement = node(random); + + while (alreadyInUse.test(replacement)) + replacement = node(random); + + Epoch epoch = epoch(random); + Set tokens = randomTokens(10, partitioner, random); + List> ranges = ranges(tokens, partitioner); + PlacementDeltas deltas = randomDeltas(ranges, random); + LockedRanges.Key key = LockedRanges.keyFor(epoch); + return new BootstrapAndReplace(epoch, + key, + tokens, + potentialNextStates[random.nextInt(3)], + new PrepareReplace.StartReplace(replaced, replacement, deltas, key), + new PrepareReplace.MidReplace(replaced, replacement, deltas, key), + new PrepareReplace.FinishReplace(replaced, replacement, deltas, key), + true, + true); + } + + public static UnbootstrapAndLeave unbootstrapAndLeave(IPartitioner partitioner, Random random, Predicate alreadyInUse) + { + Transformation.Kind[] potentialNextStates = {START_LEAVE, MID_LEAVE, FINISH_LEAVE}; + NodeId node = node(random); + while (alreadyInUse.test(node)) + node = node(random); + Epoch epoch = epoch(random); + Set tokens = randomTokens(10, partitioner, random); + List> ranges = ranges(tokens, partitioner); + PlacementDeltas deltas = randomDeltas(ranges, random); + LockedRanges.Key key = LockedRanges.keyFor(epoch); + return new UnbootstrapAndLeave(epoch, + key, + potentialNextStates[random.nextInt(3)], + new PrepareLeave.StartLeave(node, deltas, key), + new PrepareLeave.MidLeave(node, deltas, key), + new PrepareLeave.FinishLeave(node, deltas, key), + new UnbootstrapStreams()); + } + + public static Move move(IPartitioner partitioner, Random random, Predicate alreadyInUse) + { + Transformation.Kind[] potentialNextStates = {START_MOVE, MID_MOVE, FINISH_MOVE}; + NodeId node = node(random); + while (alreadyInUse.test(node)) + node = node(random); + + Epoch epoch = epoch(random); + Set tokens = randomTokens(10, partitioner, random); + List> ranges = ranges(tokens, partitioner); + PlacementDeltas deltas = randomDeltas(ranges, random); + LockedRanges.Key key = LockedRanges.keyFor(epoch); + return new Move(epoch, + key, + potentialNextStates[random.nextInt(3)], + tokens, + deltas, + new PrepareMove.StartMove(node, deltas, key), + new PrepareMove.MidMove(node, deltas, key), + new PrepareMove.FinishMove(node, tokens, deltas, key), + true); + } +} diff --git a/test/unit/org/apache/cassandra/tcm/transformations/EventsMetadataTest.java b/test/unit/org/apache/cassandra/tcm/transformations/EventsMetadataTest.java new file mode 100644 index 0000000000..11fd03c696 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/transformations/EventsMetadataTest.java @@ -0,0 +1,191 @@ +/* + * 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.tcm.transformations; + +import java.net.UnknownHostException; +import java.util.concurrent.ExecutionException; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; +import org.apache.cassandra.tcm.sequences.LeaveStreams; +import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; + +import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +// TODO: try to rewrite them as fuzz tests +public class EventsMetadataTest +{ + public static KeyspaceMetadata KSM, KSM_NTS; + private static final InetAddressAndPort node1; + private static final InetAddressAndPort node2; + + static + { + try + { + node1 = InetAddressAndPort.getByName("127.0.0.1"); + node2 = InetAddressAndPort.getByName("127.0.0.2"); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + + @BeforeClass + public static void beforeClass() + { + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + ServerTestUtils.daemonInitialization(); + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ServerTestUtils.prepareServerNoRegister(); + KSM = KeyspaceMetadata.create("ks", KeyspaceParams.simple(3)); + KSM_NTS = KeyspaceMetadata.create("ks_nts", KeyspaceParams.nts("dc1", 3, "dc2", 3)); + } + + @Before + public void before() throws ExecutionException, InterruptedException + { + ServerTestUtils.resetCMS(); + } + + @Test + public void firstRegisterTest() + { + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM); + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM_NTS); + + NodeId nodeId = ClusterMetadataTestHelper.register(node1); + // register adds to directory; + ClusterMetadata metadata = ClusterMetadata.current(); + assertEquals(node1, metadata.directory.endpoint(nodeId)); + + // should not be in tokenMap (no tokens yet) + assertTrue(metadata.tokenMap.tokens(nodeId).isEmpty()); + assertTrue(metadata.placements.get(KSM.params.replication).writes.byEndpoint().isEmpty()); + assertTrue(metadata.placements.get(KSM.params.replication).reads.byEndpoint().isEmpty()); + assertTrue(metadata.lockedRanges.locked.isEmpty()); + } + + @Test + public void prepareJoinTest() throws ExecutionException, InterruptedException + { + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM); + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM_NTS); + + NodeId nodeId = ClusterMetadataTestHelper.register(node1); + + ClusterMetadata metadata = ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareJoin(nodeId)); + + assertTrue(metadata.tokenMap.tokens(nodeId).isEmpty()); + assertEquals(NodeState.REGISTERED, metadata.directory.peerState(nodeId)); + } + + @Test + public void joinTest() throws ExecutionException, InterruptedException + { + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM); + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM_NTS); + + NodeId nodeId = ClusterMetadataTestHelper.register(node1); + + ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareJoin(nodeId)); + BootstrapAndJoin plan = (BootstrapAndJoin) ClusterMetadata.current().inProgressSequences.get(nodeId); + + ClusterMetadataService.instance().commit(plan.startJoin); + + assertTrue(ClusterMetadata.current().tokenMap.tokens(nodeId).isEmpty()); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(nodeId)); + + assertTrue(ClusterMetadata.current().placements.get(KSM.params.replication).writes.byEndpoint().containsKey(node1)); + // the first joined node gets added to the read endpoints immediately + assertTrue(ClusterMetadata.current().placements.get(KSM.params.replication).reads.byEndpoint().containsKey(node1)); + + ClusterMetadataService.instance().commit(plan.midJoin); + ClusterMetadataService.instance().commit(plan.finishJoin); + + assertEquals(NodeState.JOINED, ClusterMetadata.current().directory.peerState(nodeId)); + assertTrue(ClusterMetadata.current().lockedRanges.locked.isEmpty()); + assertTrue(ClusterMetadataTestHelper.bootstrapping(ClusterMetadata.current()).isEmpty()); + + // join a second node + ClusterMetadataTestHelper.register(node2); + nodeId = ClusterMetadata.current().directory.peerId(node2); + ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareJoin(nodeId)); + + plan = (BootstrapAndJoin) ClusterMetadata.current().inProgressSequences.get(nodeId); + + ClusterMetadataService.instance().commit(plan.startJoin); + + assertTrue(ClusterMetadata.current().tokenMap.tokens(nodeId).isEmpty()); + assertEquals(NodeState.BOOTSTRAPPING, ClusterMetadata.current().directory.peerState(nodeId)); + assertTrue(ClusterMetadata.current().placements.get(KSM.params.replication).writes.byEndpoint().containsKey(node2)); + assertFalse(ClusterMetadata.current().placements.get(KSM.params.replication).reads.byEndpoint().containsKey(node2)); + } + + @Test + public void leaveTest() throws ExecutionException, InterruptedException + { + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM); + ClusterMetadataTestHelper.addOrUpdateKeyspace(KSM_NTS); + + NodeId nodeId = ClusterMetadataTestHelper.register(node1); + + ClusterMetadataService.instance().commit(ClusterMetadataTestHelper.prepareJoin(nodeId)); + BootstrapAndJoin join = (BootstrapAndJoin) ClusterMetadata.current().inProgressSequences.get(nodeId); + + ClusterMetadataService.instance().commit(join.startJoin); + ClusterMetadataService.instance().commit(join.midJoin); + ClusterMetadataService.instance().commit(join.finishJoin); + + ClusterMetadata before = ClusterMetadata.current(); + ClusterMetadataService.instance().commit(new PrepareLeave(nodeId, true, PrepareLeaveTest.dummyPlacementProvider, LeaveStreams.Kind.UNBOOTSTRAP));; + UnbootstrapAndLeave leave = (UnbootstrapAndLeave) ClusterMetadata.current().inProgressSequences.get(nodeId); + ClusterMetadata after = ClusterMetadata.current(); + // no change in metadata after prepareLeave; + assertEquals(before.directory, after.directory); + assertEquals(before.tokenMap, after.tokenMap); + assertEquals(before.placements, after.placements); + assertEquals(before.schema, after.schema); + + ClusterMetadataService.instance().commit(leave.startLeave); + ClusterMetadataService.instance().commit(leave.midLeave); + ClusterMetadataService.instance().commit(leave.finishLeave); + + } + +} + diff --git a/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java b/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java new file mode 100644 index 0000000000..4607fa9618 --- /dev/null +++ b/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java @@ -0,0 +1,177 @@ +/* + * 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.tcm.transformations; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.Sets; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.schema.DistributedMetadataLogKeyspace; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.membership.Directory; +import org.apache.cassandra.tcm.membership.Location; +import org.apache.cassandra.tcm.membership.NodeId; +import org.apache.cassandra.tcm.membership.NodeState; +import org.apache.cassandra.tcm.ownership.DataPlacements; +import org.apache.cassandra.tcm.ownership.PlacementDeltas; +import org.apache.cassandra.tcm.ownership.PlacementProvider; +import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan; +import org.apache.cassandra.tcm.sequences.LeaveStreams; + +import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.addr; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PrepareLeaveTest +{ + private static final Map hostDc = new HashMap<>(); + private static KeyspaceMetadata KSM, KSM_NTS; + + @BeforeClass + public static void setUpClass() throws Exception + { + for (int i = 1; i <= 10; i++) + hostDc.put(InetAddress.getByName("127.0.0."+i), "dc1"); + for (int i = 11; i <= 20; i++) + hostDc.put(InetAddress.getByName("127.0.0."+i), "dc2"); + + ServerTestUtils.daemonInitialization(); + ServerTestUtils.prepareServer(); + + KSM = KeyspaceMetadata.create("ks", KeyspaceParams.simple(3)); + KSM_NTS = KeyspaceMetadata.create("ks_nts", KeyspaceParams.nts("dc1", 3, "dc2", 3)); + } + + @Test + public void testCheckRF_Simple() throws Throwable + { + Keyspaces kss = Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Sets.newHashSet(hostDc.values())), KSM); + ClusterMetadata metadata = prepMetadata(kss, 2, 2); + assertTrue(executeLeave(metadata)); + // should be rejected: + metadata = prepMetadata(kss, 1, 2); + assertFalse(executeLeave(metadata)); + } + + @Test + public void testCheckRF_NTS() throws Throwable + { + Keyspaces kss = Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Sets.newHashSet(hostDc.values())), KSM_NTS); + ClusterMetadata metadata = prepMetadata(kss, 4, 4); + assertTrue(executeLeave(metadata)); + // should be accepted (4 nodes in dc1 where we remove the host): + metadata = prepMetadata(kss, 4, 2); + assertTrue(executeLeave(metadata)); + // should be rejected + metadata = prepMetadata(kss, 3, 4); + assertFalse(executeLeave(metadata)); + } + + private boolean executeLeave(ClusterMetadata metadata) throws Throwable + { + PrepareLeave prepareLeave = new PrepareLeave(metadata.directory.peerId(InetAddressAndPort.getByName("127.0.0.1")), + false, + dummyPlacementProvider, + LeaveStreams.Kind.UNBOOTSTRAP); + + return prepareLeave.execute(metadata).isSuccess(); + } + + private ClusterMetadata prepMetadata(Keyspaces kss, int countDc1, int countDc2) throws UnknownHostException + { + DistributedSchema schema = new DistributedSchema(kss); + Directory dir = new Directory(); + for (int i = 1; i <= countDc1; i++) + { + InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0."+i); + Location l = new Location(hostDc.get(ep.getAddress()), "rack" + i); + dir = dir.with(addr(ep), l); + dir = dir.withNodeState(dir.peerId(ep), NodeState.JOINED); + dir = dir.withRackAndDC(dir.peerId(ep)); + } + for (int i = 11; i <= 10 + countDc2; i++) + { + InetAddressAndPort ep = InetAddressAndPort.getByName("127.0.0."+i); + Location l = new Location(hostDc.get(ep.getAddress()), "rack"+i); + dir = dir.with(addr(ep), l); + dir = dir.withNodeState(dir.peerId(ep), NodeState.JOINED); + dir = dir.withRackAndDC(dir.peerId(ep)); + } + ClusterMetadata.Transformer transformer = new ClusterMetadata(Murmur3Partitioner.instance, + dir, + schema).transformer(); + Random r = new Random(System.nanoTime()); + Set tokens = new HashSet<>(dir.peerIds().size()); + while(tokens.size() < dir.peerIds().size()) + tokens.add(Murmur3Partitioner.instance.getRandomToken(r)); + Iterator iter = tokens.iterator(); + for (NodeId node : dir.peerIds()) + transformer = transformer.proposeToken(node, Collections.singleton(iter.next())); + + return transformer.build().metadata; + } + + public static PlacementProvider dummyPlacementProvider = new PlacementProvider() + { + @Override + public DataPlacements calculatePlacements(Epoch epoch, List> ranges, ClusterMetadata metadata, Keyspaces keyspaces) { return null; } + + @Override + public PlacementTransitionPlan planForJoin(ClusterMetadata metadata, NodeId nodeId, Set tokens, Keyspaces keyspaces) { return null;} + + @Override + public PlacementTransitionPlan planForMove(ClusterMetadata metadata, NodeId nodeId, Set tokens, Keyspaces keyspaces) + { + return null; + } + + @Override + public PlacementTransitionPlan planForDecommission(ClusterMetadata metadata, NodeId nodeId, Keyspaces keyspaces) + { + return new PlacementTransitionPlan(PlacementDeltas.empty(), + PlacementDeltas.empty(), + PlacementDeltas.empty(), + PlacementDeltas.empty()); + } + + @Override + public PlacementTransitionPlan planForReplacement(ClusterMetadata metadata, NodeId replaced, NodeId replacement, Keyspaces keyspaces) { return null; } + }; +} diff --git a/test/unit/org/apache/cassandra/tools/JMXCompatabilityTest.java b/test/unit/org/apache/cassandra/tools/JMXCompatabilityTest.java index d4cdfae827..a2639a51a4 100644 --- a/test/unit/org/apache/cassandra/tools/JMXCompatabilityTest.java +++ b/test/unit/org/apache/cassandra/tools/JMXCompatabilityTest.java @@ -183,7 +183,24 @@ public class JMXCompatabilityTest extends CQLTester @Test public void diff40() throws Throwable { - List excludeObjects = newArrayList("org.apache.cassandra.metrics:type=BufferPool,name=(Misses|Size)" // removed in CASSANDRA-18313 + List excludeObjects = newArrayList("org.apache.cassandra.metrics:type=BufferPool,name=(Misses|Size)", // removed in CASSANDRA-18313 + // removed by CEP-21: + "org.apache.cassandra.internal:type=MigrationStage", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=ActiveTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=CompletedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=CurrentlyBlockedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=MaxPoolSize", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=MaxTasksQueued", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=PendingTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=TotalBlockedTasks", + "org.apache.cassandra.internal:type=PendingRangeCalculator", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=ActiveTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=CompletedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=CurrentlyBlockedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=MaxPoolSize", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=MaxTasksQueued", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=PendingTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=TotalBlockedTasks" ); List excludeAttributes = newArrayList("HostIdMap"); // removed in CASSANDRA-18959 List excludeOperations = newArrayList("scrub\\(p1:boolean,p2:boolean,p3:java.lang.String,p4:java.lang.String\\[\\]\\):int"); // removed in CASSANDRA-18959 @@ -200,7 +217,24 @@ public class JMXCompatabilityTest extends CQLTester @Test public void diff41() throws Throwable { - List excludeObjects = newArrayList("org.apache.cassandra.metrics:type=BufferPool,name=(Misses|Size)" // removed in CASSANDRA-18313 + List excludeObjects = newArrayList("org.apache.cassandra.metrics:type=BufferPool,name=(Misses|Size)", // removed in CASSANDRA-18313 + // removed by CEP-21: + "org.apache.cassandra.internal:type=MigrationStage", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=ActiveTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=CompletedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=CurrentlyBlockedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=MaxPoolSize", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=MaxTasksQueued", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=PendingTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=MigrationStage,name=TotalBlockedTasks", + "org.apache.cassandra.internal:type=PendingRangeCalculator", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=ActiveTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=CompletedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=CurrentlyBlockedTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=MaxPoolSize", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=MaxTasksQueued", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=PendingTasks", + "org.apache.cassandra.metrics:type=ThreadPools,path=internal,scope=PendingRangeCalculator,name=TotalBlockedTasks" ); List excludeAttributes = newArrayList("HostIdMap"); // removed in CASSANDRA-18959 List excludeOperations = newArrayList("scrub\\(p1:boolean,p2:boolean,p3:java.lang.String,p4:java.lang.String\\[\\]\\):int"); // removed in CASSANDRA-18959 diff --git a/test/unit/org/apache/cassandra/tools/StandaloneSplitterWithCQLTesterTest.java b/test/unit/org/apache/cassandra/tools/StandaloneSplitterWithCQLTesterTest.java index 40f075aa2d..56b00089db 100644 --- a/test/unit/org/apache/cassandra/tools/StandaloneSplitterWithCQLTesterTest.java +++ b/test/unit/org/apache/cassandra/tools/StandaloneSplitterWithCQLTesterTest.java @@ -20,13 +20,17 @@ package org.apache.cassandra.tools; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.junit.After; import org.junit.Before; +import org.apache.cassandra.db.lifecycle.Tracker; import org.apache.cassandra.io.util.File; + import org.junit.Test; import org.apache.cassandra.cql3.CQLTester; @@ -49,10 +53,26 @@ public class StandaloneSplitterWithCQLTesterTest extends CQLTester public void before() throws Throwable { setupTestSstables(); + // Stop server as this is exercising an offline tool tearDownClass(); SSTableReader.resetTidying(); } + @After + public void unsafeRemoveSSTables() throws Throwable + { + // Before resetting the CMS in CQLTester::afterClass, manually remove the original SSTables from the + // CFS. If we don't do this, restoring the schema to a pre-test state causes the CFS to be dropped + // which attempts to remove the SSTables in the tracker. Because we've unsafely modified these with + // a tool that should only be used offline, this causes an error in test tear down. In a real node, + // running the tool while offline, or even just restarting the node after the tool has been unsafely + // run like this, would avoid/fix this issue. + Tracker tracker = getCurrentColumnFamilyStore(KEYSPACE).getTracker(); + Set toRemove = new HashSet<>(); + tracker.getView().allKnownSSTables().forEach(toRemove::add); + tracker.removeUnsafe(toRemove); + } + @Test public void testMinFileSizeCheck() throws Throwable { diff --git a/test/unit/org/apache/cassandra/tools/StandaloneVerifierOnSSTablesTest.java b/test/unit/org/apache/cassandra/tools/StandaloneVerifierOnSSTablesTest.java index b1eb139004..8c88d41eb8 100644 --- a/test/unit/org/apache/cassandra/tools/StandaloneVerifierOnSSTablesTest.java +++ b/test/unit/org/apache/cassandra/tools/StandaloneVerifierOnSSTablesTest.java @@ -28,6 +28,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.UpdateBuilder; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.db.ColumnFamilyStore; @@ -71,7 +72,7 @@ public class StandaloneVerifierOnSSTablesTest extends OfflineToolUtils // for the check version to work CassandraRelevantProperties.PARTITIONER.setString("org.apache.cassandra.dht.ByteOrderedPartitioner"); properties = new WithProperties().set(TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST, true); - SchemaLoader.loadSchema(); + ServerTestUtils.prepareServerNoRegister(); StorageService.instance.initServer(); } diff --git a/test/unit/org/apache/cassandra/tools/SystemExitException.java b/test/unit/org/apache/cassandra/tools/SystemExitException.java index 0d92746923..a879cce9f3 100644 --- a/test/unit/org/apache/cassandra/tools/SystemExitException.java +++ b/test/unit/org/apache/cassandra/tools/SystemExitException.java @@ -18,6 +18,9 @@ package org.apache.cassandra.tools; +import org.apache.cassandra.utils.Shared; + +@Shared public class SystemExitException extends Error { public final int status; diff --git a/test/unit/org/apache/cassandra/tools/TopPartitionsTest.java b/test/unit/org/apache/cassandra/tools/TopPartitionsTest.java index d6fc065685..e673f68912 100644 --- a/test/unit/org/apache/cassandra/tools/TopPartitionsTest.java +++ b/test/unit/org/apache/cassandra/tools/TopPartitionsTest.java @@ -35,12 +35,11 @@ import com.google.common.collect.Lists; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.metrics.Sampler; -import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.Util; @@ -62,8 +61,8 @@ public class TopPartitionsTest @BeforeClass public static void loadSchema() throws ConfigurationException { - SchemaLoader.prepareServer(); - SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1)); + ServerTestUtils.prepareServerNoRegister(); + executeInternal(format("CREATE KEYSPACE %s WITH replication = {'class':'SimpleStrategy', 'replication_factor':1}", KEYSPACE)); executeInternal(format("CREATE TABLE %s.%s (k text, c text, v text, PRIMARY KEY (k, c))", KEYSPACE, TABLE)); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java index ec415df09b..091a439a8d 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.cql3.CIDR; @@ -45,9 +44,7 @@ public class CIDRFilteringStatsTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - AuthCacheService.initializeAndRegisterCaches(); String KS_NAME = SchemaConstants.VIRTUAL_VIEWS; VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, diff --git a/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java index 4ec303068f..d8e2a5d2c7 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java @@ -27,7 +27,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.cql3.CIDR; import org.apache.cassandra.cql3.CQLTester; @@ -40,10 +39,7 @@ public class DropCIDRGroupTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - AuthCacheService.initializeAndRegisterCaches(); - startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GetAuthCacheConfigTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GetAuthCacheConfigTest.java index 90484e3c58..ae82602b4e 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/GetAuthCacheConfigTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/GetAuthCacheConfigTest.java @@ -40,7 +40,6 @@ public class GetAuthCacheConfigTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); requireNetwork(); startJMXServer(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java index e8844053c3..988418ca9c 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java @@ -27,7 +27,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.cql3.CIDR; import org.apache.cassandra.cql3.CQLTester; @@ -40,10 +39,7 @@ public class GetCIDRGroupsOfIPTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - AuthCacheService.initializeAndRegisterCaches(); - startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GossipInfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GossipInfoTest.java index 7ae4b77b74..57846060b9 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/GossipInfoTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/GossipInfoTest.java @@ -99,7 +99,6 @@ public class GossipInfoTest extends CQLTester Assertions.assertThat(stdout).contains("/127.0.0.1"); Assertions.assertThat(stdout).containsPattern("\\s+generation:[0-9]+"); Assertions.assertThat(stdout).containsPattern("heartbeat:[0-9]+"); - Assertions.assertThat(stdout).containsPattern("STATUS:[0-9]+:NORMAL," + token); Assertions.assertThat(stdout).containsPattern("SCHEMA:.+"); Assertions.assertThat(stdout).containsPattern("DC:[0-9]+:datacenter1"); Assertions.assertThat(stdout).containsPattern("RACK:[0-9]+:rack1"); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java index ec75c587b0..3b43146185 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java @@ -23,7 +23,6 @@ import java.net.InetSocketAddress; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.IRoleManager; @@ -45,13 +44,11 @@ public class InvalidateCIDRPermissionsCacheTest extends CQLTester public static void setup() throws Exception { DatabaseDescriptor.setRolesValidity(Integer.MAX_VALUE-1); - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); - AuthCacheService.initializeAndRegisterCaches(); startJMXServer(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCredentialsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCredentialsCacheTest.java index d83c846840..a5ea607b63 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCredentialsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCredentialsCacheTest.java @@ -45,13 +45,10 @@ public class InvalidateCredentialsCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); - PasswordAuthenticator passwordAuthenticator = (PasswordAuthenticator) DatabaseDescriptor.getAuthenticator(); roleANegotiator = passwordAuthenticator.newSaslNegotiator(null); roleANegotiator.evaluateResponse(new PlainTextAuthProvider(ROLE_A.getRoleName(), "ignored") @@ -61,7 +58,6 @@ public class InvalidateCredentialsCacheTest extends CQLTester roleBNegotiator.evaluateResponse(new PlainTextAuthProvider(ROLE_B.getRoleName(), "ignored") .newAuthenticator((EndPoint) null, null) .initialResponse()); - requireNetwork(); startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateJmxPermissionsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateJmxPermissionsCacheTest.java index 54f2de756c..63aaf1e51b 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateJmxPermissionsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateJmxPermissionsCacheTest.java @@ -21,6 +21,7 @@ package org.apache.cassandra.tools.nodetool; import java.util.Set; import javax.security.auth.Subject; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -49,24 +50,26 @@ public class InvalidateJmxPermissionsCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); + AuthCacheService.initializeAndRegisterCaches(); + requireNetwork(); + startJMXServer(); + } + @Before + public void grantInitialPermissions() + { + // Because we reset the CMS in CQLTester::afterTest, the per-test keyspaces created in CQLTester::beforeTest + // get dropped and re-created for every individual test. This means we need to recreate the perms here (we + // could markCMS() after granting the first time and add a flag to avoid re-granting but this is simpler). JMXResource rootJmxResource = JMXResource.root(); Set jmxPermissions = rootJmxResource.applicablePermissions(); - IAuthorizer authorizer = DatabaseDescriptor.getAuthorizer(); authorizer.grant(AuthenticatedUser.SYSTEM_USER, jmxPermissions, rootJmxResource, ROLE_A); authorizer.grant(AuthenticatedUser.SYSTEM_USER, jmxPermissions, rootJmxResource, ROLE_B); - - AuthCacheService.initializeAndRegisterCaches(); - - requireNetwork(); - startJMXServer(); } @Test diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateNetworkPermissionsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateNetworkPermissionsCacheTest.java index 4122066bca..7ef5dbeebb 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateNetworkPermissionsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateNetworkPermissionsCacheTest.java @@ -39,14 +39,11 @@ public class InvalidateNetworkPermissionsCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); AuthCacheService.initializeAndRegisterCaches(); - requireNetwork(); startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCacheTest.java index c8affb2eff..895fe1da56 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCacheTest.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -54,28 +55,35 @@ public class InvalidatePermissionsCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); AuthCacheService.initializeAndRegisterCaches(); + requireNetwork(); + startJMXServer(); + } + @Before + public void grantInitialPermissions() + { + // Because we reset the CMS in CQLTester::afterTest, the per-test keyspaces created in CQLTester::beforeTest + // get dropped and re-created for every individual test. This means we need to recreate the perms here (we + // could markCMS() after granting the first time and add a flag to avoid re-granting but this is simpler). List resources = Arrays.asList( - DataResource.root(), - DataResource.keyspace(KEYSPACE), - DataResource.allTables(KEYSPACE), - DataResource.table(KEYSPACE, "t1"), - RoleResource.root(), - RoleResource.role("role_x"), - FunctionResource.root(), - FunctionResource.keyspace(KEYSPACE), - // Particular function is excluded from here and covered by a separate test because in order to grant - // permissions we need to have a function registered. However, the function cannot be registered via - // CQLTester.createFunction from static contex. That's why we initialize it in a separate test case. - JMXResource.root(), - JMXResource.mbean("org.apache.cassandra.auth:type=*")); + DataResource.root(), + DataResource.keyspace(KEYSPACE), + DataResource.allTables(KEYSPACE), + DataResource.table(KEYSPACE, "t1"), + RoleResource.root(), + RoleResource.role("role_x"), + FunctionResource.root(), + FunctionResource.keyspace(KEYSPACE), + // Particular function is excluded from here and covered by a separate test because in order to grant + // permissions we need to have a function registered. However, the function cannot be registered via + // CQLTester.createFunction from static contex. That's why we initialize it in a separate test case. + JMXResource.root(), + JMXResource.mbean("org.apache.cassandra.auth:type=*")); IAuthorizer authorizer = DatabaseDescriptor.getAuthorizer(); for (IResource resource : resources) @@ -84,9 +92,6 @@ public class InvalidatePermissionsCacheTest extends CQLTester authorizer.grant(AuthenticatedUser.SYSTEM_USER, permissions, resource, ROLE_A); authorizer.grant(AuthenticatedUser.SYSTEM_USER, permissions, resource, ROLE_B); } - - requireNetwork(); - startJMXServer(); } @Test diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateRolesCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateRolesCacheTest.java index 08ba676d3d..f78c393732 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateRolesCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateRolesCacheTest.java @@ -39,14 +39,11 @@ public class InvalidateRolesCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); AuthCacheService.initializeAndRegisterCaches(); - requireNetwork(); startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java index a6b377d9dc..f64d3a0509 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java @@ -28,7 +28,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.cql3.CIDR; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.tools.ToolRunner; @@ -41,17 +40,14 @@ public class ListCIDRGroupTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - AuthCacheService.initializeAndRegisterCaches(); - startJMXServer(); } @Before public void before() { - Map> cidrsMapping = new HashMap>() + Map> cidrsMapping = new HashMap<>() {{ put("test1", Collections.singletonList(CIDR.getInstance("10.11.12.0/24"))); put("test2", Arrays.asList(CIDR.getInstance("11.11.12.0/24"), CIDR.getInstance("12.11.12.0/18"))); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java index 3237d77c32..f07c1ba122 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java @@ -24,7 +24,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.auth.AuthTestUtils; import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.IRoleManager; @@ -46,13 +45,11 @@ public class ReloadCIDRGroupsCacheTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); IRoleManager roleManager = DatabaseDescriptor.getRoleManager(); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions()); roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions()); - AuthCacheService.initializeAndRegisterCaches(); startJMXServer(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/SetAuthCacheConfigTest.java b/test/unit/org/apache/cassandra/tools/nodetool/SetAuthCacheConfigTest.java index 79329c66d0..5405995751 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/SetAuthCacheConfigTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/SetAuthCacheConfigTest.java @@ -40,7 +40,6 @@ public class SetAuthCacheConfigTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); requireNetwork(); startJMXServer(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/TableHistogramsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/TableHistogramsTest.java index 733b876c09..4a233b22fa 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/TableHistogramsTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/TableHistogramsTest.java @@ -43,7 +43,8 @@ public class TableHistogramsTest extends CQLTester SchemaKeyspace.metadata().tables.size() + TraceKeyspace.TABLE_NAMES.size() + AuthKeyspace.TABLE_NAMES.size() + - SystemDistributedKeyspace.TABLE_NAMES.size(); + SystemDistributedKeyspace.TABLE_NAMES.size() + + 1; // DistributedMetadataLogKeyspace contains a single table @BeforeClass public static void setup() throws Exception diff --git a/test/unit/org/apache/cassandra/tools/nodetool/TableStatsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/TableStatsTest.java index b4e9b90f4a..11e5e1f569 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/TableStatsTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/TableStatsTest.java @@ -248,7 +248,9 @@ public class TableStatsTest extends CQLTester ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("tablestats", arg, "yaml"); tool.assertOnCleanExit(); String yaml = tool.getStdout(); - assertThatCode(() -> new Yaml().load(yaml)).doesNotThrowAnyException(); + org.yaml.snakeyaml.LoaderOptions loaderOptions = new org.yaml.snakeyaml.LoaderOptions(); + loaderOptions.setMaxAliasesForCollections(100); // we now have > 50 tables + assertThatCode(() -> new Yaml(loaderOptions).load(yaml)).doesNotThrowAnyException(); assertThat(yaml).containsPattern("sstable_count:\\s*[0-9]+") .containsPattern("old_sstable_count:\\s*[0-9]+"); }); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java index b3fbb9d34d..12f8a9b39d 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.auth.AuthCacheService; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.tools.ToolRunner; @@ -36,9 +35,7 @@ public class UpdateCIDRGroupTest extends CQLTester @BeforeClass public static void setup() throws Exception { - CQLTester.setUpClass(); CQLTester.requireAuthentication(); - AuthCacheService.initializeAndRegisterCaches(); startJMXServer(); } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java b/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java index 1dc7a30e26..f6362adf81 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java @@ -36,7 +36,6 @@ public class VerifyTest extends CQLTester @BeforeClass public static void setup() throws Exception { - SchemaLoader.prepareServer(); AuthTestUtils.LocalCassandraRoleManager roleManager = new AuthTestUtils.LocalCassandraRoleManager(); SchemaLoader.setupAuth(roleManager, new AuthTestUtils.LocalPasswordAuthenticator(), diff --git a/test/unit/org/apache/cassandra/triggers/TriggersSchemaTest.java b/test/unit/org/apache/cassandra/triggers/TriggersSchemaTest.java index 6b875a37f1..c199697ff0 100644 --- a/test/unit/org/apache/cassandra/triggers/TriggersSchemaTest.java +++ b/test/unit/org/apache/cassandra/triggers/TriggersSchemaTest.java @@ -23,17 +23,19 @@ import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaTestUtil; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.KeyspaceMetadata; -import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.Tables; import org.apache.cassandra.schema.TriggerMetadata; import org.apache.cassandra.schema.Triggers; import static org.apache.cassandra.utils.Clock.Global.nanoTime; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class TriggersSchemaTest { diff --git a/test/unit/org/apache/cassandra/triggers/TriggersTest.java b/test/unit/org/apache/cassandra/triggers/TriggersTest.java index 892a0220f9..47bf22f1a5 100644 --- a/test/unit/org/apache/cassandra/triggers/TriggersTest.java +++ b/test/unit/org/apache/cassandra/triggers/TriggersTest.java @@ -57,7 +57,7 @@ public class TriggersTest @Before public void setup() throws Exception { - StorageService.instance.initServer(0); + StorageService.instance.initServer(); String cql = String.format("CREATE KEYSPACE IF NOT EXISTS %s " + "WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}", diff --git a/test/unit/org/apache/cassandra/utils/RecomputingSupplierTest.java b/test/unit/org/apache/cassandra/utils/RecomputingSupplierTest.java deleted file mode 100644 index 77ce38d962..0000000000 --- a/test/unit/org/apache/cassandra/utils/RecomputingSupplierTest.java +++ /dev/null @@ -1,157 +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.utils; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.LockSupport; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.cassandra.Util; - -public class RecomputingSupplierTest -{ - // This test case verifies that recomputing supplier never returns out of order values during concurrent updates and - // eventually returns the most recent value. - @Test - public void recomputingSupplierTest() throws Throwable - { - ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); - ExecutorService testExecutor = Executors.newFixedThreadPool(10); - AtomicReference thrown = new AtomicReference<>(); - final AtomicLong counter = new AtomicLong(0); - - final RecomputingSupplier supplier = new RecomputingSupplier<>(() -> { - try - { - long v = counter.incrementAndGet(); - LockSupport.parkNanos(1); - // Make sure that the value still hasn't changed - Assert.assertEquals(v, counter.get()); - return v; - } - catch (Throwable e) - { - thrown.set(e); - throw new RuntimeException(e); - } - }, executor); - - for (int i = 0; i < 5; i++) - { - testExecutor.submit(() -> { - try - { - while (!Thread.interrupted() && !testExecutor.isShutdown()) - supplier.recompute(); - } - catch (Throwable e) - { - thrown.set(e); - } - }); - } - - AtomicLong lastSeen = new AtomicLong(0); - for (int i = 0; i < 5; i++) - { - testExecutor.submit(() -> { - while (!Thread.interrupted() && !testExecutor.isShutdown()) - { - try - { - long seenBeforeGet = lastSeen.get(); - Long v = supplier.get(5, TimeUnit.SECONDS); - if (v != null) - { - lastSeen.accumulateAndGet(v, Math::max); - Assert.assertTrue(String.format("Seen %d out of order. Last seen value %d", v, seenBeforeGet), - v >= seenBeforeGet); - } - } - catch (Throwable e) - { - thrown.set(e); - } - } - }); - } - - Util.spinAssertEquals(true, () -> counter.get() > 1000, 30); - testExecutor.shutdown(); - Assert.assertTrue(testExecutor.awaitTermination(30, TimeUnit.SECONDS)); - - if (thrown.get() != null) - throw new AssertionError(supplier.toString(), thrown.get()); - - Util.spinAssertEquals(true, () -> { - try - { - return supplier.get(1, TimeUnit.SECONDS) == counter.get(); - } - catch (InterruptedException | ExecutionException | TimeoutException e) - { - e.printStackTrace(); - return false; - } - }, 10); - - executor.shutdown(); - Assert.assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS)); - - if (thrown.get() != null) - throw new AssertionError(supplier.toString(), thrown.get()); - } - - @Test - public void throwingSupplier() throws InterruptedException, TimeoutException - { - ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); - - - final RecomputingSupplier supplier = new RecomputingSupplier<>(() -> { - throw new RuntimeException(); - }, executor); - - supplier.recompute(); - - try - { - supplier.get(10, TimeUnit.SECONDS); - Assert.fail("Should have thrown"); - } - catch (ExecutionException t) - { - // ignore - } - finally - { - executor.shutdown(); - executor.awaitTermination(10, TimeUnit.SECONDS); - } - } -} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapGuavaTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapGuavaTest.java new file mode 100644 index 0000000000..a80e131ad8 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapGuavaTest.java @@ -0,0 +1,78 @@ +/* + * 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.utils.btree; + +import java.util.Arrays; +import java.util.Map; + +import com.google.common.collect.BiMap; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.MapFeature; +import com.google.common.collect.testing.google.BiMapInverseTester; +import com.google.common.collect.testing.google.BiMapTestSuiteBuilder; +import com.google.common.collect.testing.google.TestBiMapGenerator; +import com.google.common.collect.testing.google.TestStringBiMapGenerator; +import com.google.common.collect.testing.testers.CollectionContainsAllTester; +import com.google.common.collect.testing.testers.CollectionContainsTester; +import com.google.common.collect.testing.testers.CollectionCreationTester; +import com.google.common.collect.testing.testers.CollectionToArrayTester; +import com.google.common.collect.testing.testers.MapCreationTester; +import com.google.common.collect.testing.testers.MapEntrySetTester; +import com.google.common.collect.testing.testers.SetHashCodeTester; + +import junit.framework.Test; // checkstyle: permit this import +import junit.framework.TestSuite; // checkstyle: permit this import + +import static com.google.common.collect.testing.Helpers.getMethod; + +public class BTreeBiMapGuavaTest +{ + public static Test suite() { + TestSuite suite = new TestSuite(); + + suite.addTest(BiMapTestSuiteBuilder.using(generator) + .named("ImmutableBiMap") + .withFeatures(CollectionSize.ANY, MapFeature.RESTRICTS_KEYS, MapFeature.REJECTS_DUPLICATES_AT_CREATION) + .suppressing(Arrays.asList(getMethod(CollectionToArrayTester.class, "testToArray_oversizedArray"), + getMethod(BiMapInverseTester.class, "testInverseSame"), + getMethod(MapEntrySetTester.class, "testContainsEntryWithIncomparableValue"), + getMethod(MapCreationTester.class, "testCreateWithNullValueUnsupported"), + getMethod(MapCreationTester.class, "testCreateWithNullKeyUnsupported"), + getMethod(CollectionContainsAllTester.class, "testContainsAll_nullAllowed"), + getMethod(CollectionCreationTester.class, "testCreateWithNull_unsupported"), + getMethod(SetHashCodeTester.class, "testHashCode"), + getMethod(CollectionContainsTester.class, "testContains_nullNotContainedButQueriesSupported"))) + .createTestSuite()); + return suite; + } + + static TestBiMapGenerator generator = new TestStringBiMapGenerator() + { + @Override + protected BiMap create(Map.Entry[] entries) + { + BTreeBiMap bimap = BTreeBiMap.empty(); + if (entries == null) + return bimap; + for (Map.Entry entry : entries) + bimap = bimap.with(entry.getKey(), entry.getValue()); + return bimap; + } + }; +} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapTest.java new file mode 100644 index 0000000000..fc7cef394b --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeBiMapTest.java @@ -0,0 +1,132 @@ +/* + * 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.utils.btree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import org.junit.Test; + +import org.apache.cassandra.utils.Pair; + +import static org.junit.Assert.assertEquals; + +public class BTreeBiMapTest +{ + @Test + public void simpleDeleteTest() + { + BTreeBiMap actual = BTreeBiMap.empty(); + actual = actual.with(1, "11"); + actual = actual.with(2, "22"); + actual = actual.with(3, "33"); + assertEquals(3, actual.keySet().size()); + assertEquals(3, actual.inverse().keySet().size()); + assertEquals("11", actual.get(1)); + assertEquals("22", actual.get(2)); + assertEquals("33", actual.get(3)); + actual = actual.without(2); + assertEquals(2, actual.keySet().size()); + assertEquals(2, actual.inverse().keySet().size()); + assertEquals("11", actual.get(1)); + assertEquals("33", actual.get(3)); + + } + + @Test(expected = IllegalArgumentException.class) + public void invertDuplicateValuesTest() + { + BTreeBiMap actual = BTreeBiMap.empty(); + actual = actual.with(1, "11"); + actual = actual.with(2, "22"); + actual = actual.with(3, "11"); + } + + @Test + public void randomTest() + { + long seed = 0; + try + { + for (int x = 0; x < 100; x++) + { + seed = System.currentTimeMillis(); + Random r = new Random(seed); + + int listSize = 100 + r.nextInt(200); + Set unique = new HashSet<>(listSize); + + while (unique.size() < listSize) + unique.add(r.nextInt(10000)); + + // need unique keys and unique values; + List rawKeys = new ArrayList<>(unique); + List rawValues = new ArrayList<>(unique); + Collections.shuffle(rawKeys); + List> raw = new ArrayList<>(); + for (int i = 0; i < listSize; i++) + raw.add(Pair.create(rawKeys.get(i), rawValues.get(i))); + + BiMap expected = HashBiMap.create(listSize); + BTreeBiMap actual = BTreeBiMap.empty(); + + for (Pair p : raw) + { + expected.put(p.left, p.right); + actual = actual.with(p.left, p.right); + + if (expected.size() > 5 && r.nextInt(10) < 4) + { + int toRemove = r.nextInt(expected.size()); + expected.remove(raw.get(toRemove).left); + actual = actual.without(raw.get(toRemove).left); + } + } + assertEqual(expected, actual); + assertEqual(expected.inverse(), actual.inverse()); + assertEqual(expected, actual.inverse().inverse()); + assertEqual(expected.inverse().inverse(), actual); + } + } + catch (Throwable t) + { + throw new RuntimeException("Seed = "+seed, t); + } + } + + private void assertEqual(BiMap expected, BiMap actual) + { + assertEquals(expected.size(), actual.size()); + + Iterator> expectedIter = expected.entrySet().iterator(); + while (expectedIter.hasNext()) + { + Map.Entry e = expectedIter.next(); + assertEquals(expected + " \n " + actual, e.getValue(), actual.get(e.getKey())); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeMapGuavaTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeMapGuavaTest.java new file mode 100644 index 0000000000..41416a3d8a --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeMapGuavaTest.java @@ -0,0 +1,128 @@ +/* + * 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.utils.btree; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.testing.*; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.testers.MapEntrySetTester; + +import junit.framework.Test; // checkstyle: permit this import +import junit.framework.TestSuite; // checkstyle: permit this import + +import static com.google.common.collect.testing.Helpers.getMethod; + +public class BTreeMapGuavaTest +{ + public static Test suite() + { + return new BTreeMapGuavaTest().allTests(); + } + + public Test allTests() + { + TestSuite suite = new TestSuite("btree map tests"); + // todo; add a mutable wrapper and run the full map tests + // todo; add a navigable wrapper and run the full navigable map tests + suite.addTest(testsForUnmodifiableMap()); + return suite; + } + + public Test testsForUnmodifiableMap() { + return MapTestSuiteBuilder.using( + new TestStringMapGenerator() { + @Override + protected Map create(Map.Entry[] entries) + { + return populate(entries); + } + }) + .named("unmodifiableMap/BTreeMap") + .withFeatures( + CollectionSize.ANY) + .suppressing(Arrays.asList(getMethod(MapEntrySetTester.class, "testContainsEntryWithIncomparableValue"))) + .createTestSuite(); + } + + private static Map populate(Map.Entry[] entries) + { + BTreeMap map = BTreeMap.empty(); + for (Map.Entry entry : entries) + map = map.withForce(entry.getKey(), entry.getValue()); + return new BTreeMapWrapper<>(map); + } + + // BTreeSet toString / toArray / hashCode are non-standard, wrap them with the set from BTreeSetGuavaTest; + private static class BTreeMapWrapper, V> extends AbstractMap + { + private final BTreeMap map; + + public BTreeMapWrapper(BTreeMap map) + { + this.map = map; + } + + @Override + public Set> entrySet() + { + return new BTreeSetGuavaTest.BTreeSetWithClassicToArray<>((BTreeSet>)map.entrySet()); + } + + @Override + public Set keySet() + { + return new BTreeSetGuavaTest.BTreeSetWithClassicToArray<>((BTreeSet)map.keySet()); + } + + public V get(K key) + { + return map.get(key); + } + + public Collection values() + { + return map.values(); + } + + public boolean containsValue(Object value) + { + return map.containsValue(value); + } + + public boolean containsKey(Object key) + { + return map.containsKey(key); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public int size() + { + return map.size(); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeMapTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeMapTest.java new file mode 100644 index 0000000000..478affa640 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeMapTest.java @@ -0,0 +1,133 @@ +/* + * 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.utils.btree; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; + +import org.junit.Test; + +import org.apache.cassandra.utils.Pair; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BTreeMapTest +{ + @Test(expected = IllegalStateException.class) + public void testDuplicates() + { + BTreeMap map = BTreeMap.empty(); + + map = map.with(1, "hello"); + map = map.with(2, "bye"); + map = map.with(1, "aaaa"); + } + + @Test + public void keySetTest() + { + BTreeMap map = BTreeMap.empty(); + + map = map.with(1, "hello"); + map = map.with(2, "bye"); + + System.out.println(map.keySet()); + + } + + + @Test + public void randomTest() + { + long seed = 0; + try + { + for (int i = 0; i < 100; i++) + { + seed = System.currentTimeMillis(); + Random r = new Random(seed); + int listSize = 100 + r.nextInt(500); // todo: increase after rebase due to BTreeRemoval bug + List> raw = new ArrayList<>(listSize); + + for (int j = 0; j < listSize; j++) + raw.add(Pair.create(r.nextInt(10000), r.nextInt())); + + TreeMap expected = new TreeMap<>(); + BTreeMap actual = BTreeMap.empty(); + for (Pair p : raw) + { + expected.put(p.left, p.right); + actual = actual.withForce(p.left, p.right); + if (expected.size() > 5 && r.nextInt(10) < 4) + { + int toRemove = r.nextInt(expected.size()); + expected.remove(raw.get(toRemove).left); + actual = actual.without(raw.get(toRemove).left); + } + } + assertEqual(expected, actual); + } + } + catch (Throwable t) + { + throw new AssertionError("seed = "+seed, t); + } + } + + private void assertEqual(TreeMap expected, BTreeMap actual) + { + assertEquals(actual + "\n" + expected , expected.size(), actual.size()); + + Iterator> expectedIter = expected.entrySet().iterator(); + Iterator> actualIter = actual.entrySet().iterator(); + while (expectedIter.hasNext()) + { + assertTrue(actualIter.hasNext()); + Map.Entry e = expectedIter.next(); + Map.Entry a = actualIter.next(); + assertEquals(actual + "\n" + expected, e, a); + } + + Iterator actualKeyIter = actual.keySet().iterator(); + Iterator expectedKeyIter = expected.keySet().iterator(); + while (expectedKeyIter.hasNext()) + { + assertTrue(actualKeyIter.hasNext()); + assertEquals(actualKeyIter.next(), expectedKeyIter.next()); + } + + List actualValues = new ArrayList<>(actual.values()); + actualValues.sort(Comparator.naturalOrder()); + List expectedValues = new ArrayList<>(expected.values()); + expectedValues.sort(Comparator.naturalOrder()); + Iterator actualValueIterator = actualValues.iterator(); + Iterator expectedValueIter = expectedValues.iterator(); + while (actualValueIterator.hasNext()) + { + assertTrue(expectedValueIter.hasNext()); + assertEquals(actualValueIterator.next(), expectedValueIter.next()); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeMultimapTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeMultimapTest.java new file mode 100644 index 0000000000..c3b980ac37 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeMultimapTest.java @@ -0,0 +1,118 @@ +/* + * 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.utils.btree; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Sets; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import org.apache.cassandra.utils.Pair; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class BTreeMultimapTest +{ + @Test + public void basicTest() + { + BTreeMultimap map = BTreeMultimap.empty(); + map = map.with("hello", 123); + map = map.with("hello", 124); + map = map.with("hello", 125); + assertEquals(3, map.size()); + assertEquals(Sets.newHashSet(123, 124, 125), map.get("hello")); + map = map.without("hello", 124); + assertEquals(2, map.size()); + assertEquals(Sets.newHashSet(123, 125), map.get("hello")); + map = map.without("hello", 123); + assertEquals(1, map.size()); + assertEquals(Sets.newHashSet(125), map.get("hello")); + map = map.without("hello", 125); + assertEquals(0, map.size()); + assertFalse(map.containsKey("hello")); + } + + @Test + public void randomTest() + { + // todo; BTree removals are currently broken - increase these numbers after rebase to current trunk (and change seed below) + int insertCount = 200; + for (int x = 0; x < 200; x++) + { + List> inserted = new ArrayList<>(); + BiMap ref = HashBiMap.create(); + BTreeBiMap test = BTreeBiMap.empty(); + boolean inversed = false; + long seed = 1; +// long seed = System.currentTimeMillis(); + Random r = new Random(seed); + while (inserted.size() < insertCount) + { + double d = r.nextDouble(); + if (d < 0.01) + { + ref = ref.inverse(); + test = (BTreeBiMap) test.inverse(); + inversed = !inversed; + } + else if (d < 0.02) + { + if (inserted.size() > 0) + { + Pair p = inserted.get(r.nextInt(inserted.size())); + String key = inversed ? p.right : p.left; + ref.remove(key); + test = test.without(key); + inserted.remove(p); + } + } + else + { + String key = randomStr(10, r); + String value = randomStr(10, r); + ref.put(key, value); + test = test.with(key, value); + Pair p = Pair.create(inversed ? value : key, inversed ? key : value); + inserted.add(p); + } + assertEqual(ref, test); + } + } + } + + private void assertEqual(BiMap a, BTreeBiMap b) + { + assertEquals(a.size(), b.size()); + for (Map.Entry expect : a.entrySet()) + assertEquals(b.get(expect.getKey()), expect.getValue()); + } + + private String randomStr(int len, Random r) + { + return RandomStringUtils.random(len, 0, 0, true, true, null, r); + } +} diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeSetGuavaTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeSetGuavaTest.java new file mode 100644 index 0000000000..bf95c88266 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeSetGuavaTest.java @@ -0,0 +1,121 @@ +/* + * 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.utils.btree; + +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +import com.google.common.collect.testing.SetTestSuiteBuilder; +import com.google.common.collect.testing.TestStringSetGenerator; +import com.google.common.collect.testing.features.CollectionSize; + +import junit.framework.Test; // checkstyle: permit this import +import junit.framework.TestSuite; // checkstyle: permit this import + +import static java.util.Comparator.naturalOrder; + +public class BTreeSetGuavaTest +{ + public static Test suite() + { + return new BTreeSetGuavaTest().allTests(); + } + + public Test allTests() + { + TestSuite suite = new TestSuite("btree map tests"); + suite.addTest(testsForUnmodifiableSet()); + // todo: SortedSetTestSuiteBuilder + return suite; + } + + public Test testsForUnmodifiableSet() + { + return SetTestSuiteBuilder.using( + new TestStringSetGenerator() { + @Override + public Set create(String[] elements) + { + return populate(elements); + } + }) + .named("unmodifiableSet/HashSet") + .withFeatures(CollectionSize.ANY) + .suppressing(Collections.emptySet()) + .createTestSuite(); + } + + private static Set populate(String[] entries) + { + BTreeSetWithClassicToArray set = BTreeSetWithClassicToArray.empty(naturalOrder()); + set = set.update(new TreeSet<>(Arrays.asList(entries))); + return set; + } + + /** + * our hashCode / toString / toArray are not correct according to guava, using AbstractSet ones here to get the test to pass + */ + public static class BTreeSetWithClassicToArray extends AbstractSet + { + private final BTreeSet wrapped; + + public BTreeSetWithClassicToArray(BTreeSet s) + { + wrapped = s; + } + + public BTreeSetWithClassicToArray update(Collection entries) + { + return new BTreeSetWithClassicToArray<>(wrapped.with(entries)); + } + + public static > BTreeSetWithClassicToArray empty(Comparator comparator) + { + return new BTreeSetWithClassicToArray(BTreeSet.empty(comparator)); + } + + @Override + public Iterator iterator() + { + return wrapped.iterator(); + } + + @Override + public int size() + { + return wrapped.size(); + } + + public boolean contains(Object x) + { + return wrapped.contains(x); + } + + public boolean isEmpty() + { + return wrapped.isEmpty(); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java b/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java index bde05864af..7255564608 100644 --- a/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java +++ b/test/unit/org/apache/cassandra/utils/concurrent/AbstractTransactionalTest.java @@ -23,7 +23,8 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.Assert; -import org.apache.cassandra.config.DatabaseDescriptor; + +import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.db.commitlog.CommitLog; @Ignore @@ -32,7 +33,7 @@ public abstract class AbstractTransactionalTest @BeforeClass public static void setupDD() { - DatabaseDescriptor.daemonInitialization(); + ServerTestUtils.prepareServerNoRegister(); CommitLog.instance.start(); } diff --git a/tools/bin/addtocmstool b/tools/bin/addtocmstool new file mode 100755 index 0000000000..3721639b6f --- /dev/null +++ b/tools/bin/addtocmstool @@ -0,0 +1,49 @@ +#!/bin/sh + +# 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. + +if [ "x$CASSANDRA_INCLUDE" = "x" ]; then + # Locations (in order) to use when searching for an include file. + for include in "`dirname "$0"`/cassandra.in.sh" \ + "$HOME/.cassandra.in.sh" \ + /usr/share/cassandra/cassandra.in.sh \ + /usr/local/share/cassandra/cassandra.in.sh \ + /opt/cassandra/cassandra.in.sh; do + if [ -r "$include" ]; then + . "$include" + break + fi + done +elif [ -r "$CASSANDRA_INCLUDE" ]; then + . "$CASSANDRA_INCLUDE" +fi + +if [ -z "$CLASSPATH" ]; then + echo "You must set the CLASSPATH var" >&2 + exit 1 +fi + +if [ "x$MAX_HEAP_SIZE" = "x" ]; then + MAX_HEAP_SIZE="256M" +fi + +"$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE \ + -Dcassandra.storagedir="$cassandra_storagedir" \ + -Dlog4j.configurationFile=log4j2-tools.xml \ + org.apache.cassandra.tools.TransformClusterMetadataHelper "$@" + +# vi:ai sw=4 ts=4 tw=0 et diff --git a/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java b/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java index e25f95e2c5..c8cc0e3daf 100644 --- a/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java +++ b/tools/stress/src/org/apache/cassandra/io/sstable/StressCQLSSTableWriter.java @@ -36,9 +36,9 @@ import org.apache.commons.lang3.ArrayUtils; import org.antlr.runtime.RecognitionException; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLFragmentParser; -import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.CqlParser; +import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.UpdateParameters; import org.apache.cassandra.cql3.functions.types.TypeCodec; @@ -61,9 +61,10 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaTransformations; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.schema.Tables; import org.apache.cassandra.schema.Types; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.JavaDriverUtils; @@ -619,14 +620,14 @@ public class StressCQLSSTableWriter implements Closeable */ public static ColumnFamilyStore createOfflineTable(CreateTableStatement.Raw schemaStatement, List typeStatements, List directoryList) { + ClusterMetadata prev = ClusterMetadata.current(); String keyspace = schemaStatement.keyspace(); - Schema.instance.transform(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1)), true)); + KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1)); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(ksm)); Types types = createTypes(keyspace, typeStatements); - Schema.instance.transform(SchemaTransformations.addTypes(types, true)); - - KeyspaceMetadata ksm = Schema.instance.getKeyspaceMetadata(keyspace); + Schema.instance.submit(SchemaTransformations.addTypes(types, true)); TableMetadata tableMetadata = ksm.tables.getNullable(schemaStatement.table()); if (tableMetadata != null) @@ -640,17 +641,15 @@ public class StressCQLSSTableWriter implements Closeable tableMetadata = statement.builder(ksm.types) .id(deterministicId(schemaStatement.keyspace(), schemaStatement.table())) .build(); - + Tables tables = Tables.of(tableMetadata); + KeyspaceMetadata updated = ksm.withSwapped(tables); + Schema.instance.submit((metadata) -> metadata.schema.getKeyspaces().withAddedOrUpdated(updated)); + ClusterMetadata.current().schema.initializeKeyspaceInstances(prev.schema, false); Keyspace.setInitialized(); Directories directories = new Directories(tableMetadata, directoryList.stream().map(f -> new Directories.DataDirectory(new org.apache.cassandra.io.util.File(f.toPath()))).collect(Collectors.toList())); Keyspace ks = Keyspace.openWithoutSSTables(keyspace); - ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(ks, tableMetadata.name, TableMetadataRef.forOfflineTools(tableMetadata), directories, false, false, true); - - ks.initCfCustom(cfs); - Schema.instance.transform(SchemaTransformations.addTable(tableMetadata, true)); - - return cfs; + return ColumnFamilyStore.createColumnFamilyStore(ks, tableMetadata.name, tableMetadata, directories, false, false); } private static TableId deterministicId(String keyspace, String table) diff --git a/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java index d284a1b6d6..426ccae58f 100644 --- a/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java +++ b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java @@ -45,6 +45,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.lifecycle.LifecycleTransaction; @@ -57,12 +58,12 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.locator.TokenMetadata; -import org.apache.cassandra.service.StorageService; import org.apache.cassandra.stress.generate.PartitionGenerator; import org.apache.cassandra.stress.generate.SeedManager; import org.apache.cassandra.stress.operations.userdefined.SchemaInsert; import org.apache.cassandra.stress.settings.StressSettings; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tools.nodetool.CompactionStats; import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; import org.apache.cassandra.utils.FBUtilities; @@ -126,7 +127,7 @@ public abstract class CompactionStress implements Runnable ColumnFamilyStore initCf(StressProfile stressProfile, boolean loadSSTables) { - generateTokens(stressProfile.seedStr, StorageService.instance.getTokenMetadata(), numTokens); + generateTokens(stressProfile.seedStr, numTokens); CreateTableStatement.Raw createStatement = stressProfile.getCreateStatement(); List dataDirectories = getDataDirectories(); @@ -193,12 +194,12 @@ public abstract class CompactionStress implements Runnable * We need consistency to write and compact the same data offline * in the case of a range aware sstable writer. */ - private void generateTokens(String seed, TokenMetadata tokenMetadata, Integer numTokens) + private void generateTokens(String seed, Integer numTokens) { Random random = new Random(seed.hashCode()); - IPartitioner p = tokenMetadata.partitioner; - tokenMetadata.clearUnsafe(); + IPartitioner p = ClusterMetadata.current().tokenMap.partitioner(); + // tokenMetadata.clearUnsafe(); for (int i = 1; i <= numTokens; i++) { InetAddressAndPort addr = FBUtilities.getBroadcastAddressAndPort(); @@ -206,7 +207,7 @@ public abstract class CompactionStress implements Runnable for (int j = 0; j < numTokens; ++j) tokens.add(p.getRandomToken(random)); - tokenMetadata.updateNormalTokens(tokens, addr); +// tokenMetadata.updateNormalTokens(tokens, addr); } } @@ -225,6 +226,7 @@ public abstract class CompactionStress implements Runnable public void run() { + ClusterMetadataService.initializeForTools(true); //Setup CompactionManager.instance.setMaximumCompactorThreads(threads); CompactionManager.instance.setCoreCompactorThreads(threads); @@ -301,6 +303,8 @@ public abstract class CompactionStress implements Runnable public void run() { + ClusterMetadataService.initializeForTools(true); + Keyspace.setInitialized(); StressProfile stressProfile = getStressProfile(); ColumnFamilyStore cfs = initCf(stressProfile, false); Directories directories = cfs.getDirectories();