|
| 1 | +/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ |
| 2 | +/* |
| 3 | + * Copyright 2018 Couchbase, Inc |
| 4 | + * |
| 5 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | + * you may not use this file except in compliance with the License. |
| 7 | + * You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +/* |
| 19 | + * Unit tests for DCP which connecting a DCP Producer to a DCP Consumer. |
| 20 | + */ |
| 21 | + |
| 22 | +#include <memcached/protocol_binary.h> |
| 23 | +#include <programs/engine_testapp/mock_server.h> |
| 24 | +#include <tests/mock/mock_dcp_consumer.h> |
| 25 | +#include <tests/mock/mock_dcp_producer.h> |
| 26 | +#include <tests/mock/mock_stream.h> |
| 27 | + |
| 28 | +#include "checkpoint.h" |
| 29 | +#include "evp_store_single_threaded_test.h" |
| 30 | +#include "test_helpers.h" |
| 31 | + |
| 32 | +/** |
| 33 | + * Test fixture which creates two ep-engine (bucket) instances, using one |
| 34 | + * as a source for DCP replication and the second as the destination. |
| 35 | + */ |
| 36 | +class DCPLoopbackStreamTest : public SingleThreadedKVBucketTest { |
| 37 | +protected: |
| 38 | + void SetUp() override { |
| 39 | + SingleThreadedKVBucketTest::SetUp(); |
| 40 | + |
| 41 | + // In addition to the initial engine which is created; we also need |
| 42 | + // to create a second bucket instance for the destination (replica) |
| 43 | + // vBucket. |
| 44 | + std::string config = config_string; |
| 45 | + if (config.size() > 0) { |
| 46 | + config += ";"; |
| 47 | + } |
| 48 | + config += "dbname=" + std::string(test_dbname) + "-replica"; |
| 49 | + replicaEngine = SynchronousEPEngine::build(config); |
| 50 | + |
| 51 | + setupProducerAndConsumerStreams(); |
| 52 | + } |
| 53 | + |
| 54 | + void setupProducerAndConsumerStreams() { |
| 55 | + // Setup the source (active) and destination (replica) Buckets. |
| 56 | + EXPECT_EQ(ENGINE_SUCCESS, |
| 57 | + engine->getKVBucket()->setVBucketState( |
| 58 | + vbid, |
| 59 | + vbucket_state_active, /*transfer*/ |
| 60 | + false)); |
| 61 | + EXPECT_EQ(ENGINE_SUCCESS, |
| 62 | + replicaEngine->getKVBucket()->setVBucketState( |
| 63 | + vbid, |
| 64 | + vbucket_state_replica, /*transfer*/ |
| 65 | + false)); |
| 66 | + |
| 67 | + auto& sourceVb = *engine->getVBucket(vbid); |
| 68 | + |
| 69 | + // Add some items to replicate / takeover to the source Bucket. |
| 70 | + store_item(vbid, makeStoredDocKey("key1"), "value"); |
| 71 | + store_item(vbid, makeStoredDocKey("key2"), "value"); |
| 72 | + store_item(vbid, makeStoredDocKey("key3"), "value"); |
| 73 | + |
| 74 | + // Setup the consumer. |
| 75 | + consumer = std::make_shared<MockDcpConsumer>( |
| 76 | + *replicaEngine, cookie, "test_consumer"); |
| 77 | + EXPECT_EQ(ENGINE_SUCCESS, |
| 78 | + consumer->addStream( |
| 79 | + /*opaque*/ 0, vbid, DCP_ADD_STREAM_FLAG_TAKEOVER)); |
| 80 | + consumerStream = consumer->getVbucketStream(vbid).get(); |
| 81 | + |
| 82 | + // Need to discard the first message from the consumerStream (the |
| 83 | + // StreamRequest), as we'll manually set that up in the producer. |
| 84 | + { |
| 85 | + std::unique_ptr<DcpResponse> streamRequest(consumerStream->next()); |
| 86 | + EXPECT_NE(nullptr, streamRequest); |
| 87 | + EXPECT_EQ(DcpResponse::Event::StreamReq, streamRequest->getEvent()); |
| 88 | + } |
| 89 | + |
| 90 | + // Create the Dcp producer. |
| 91 | + producer = SingleThreadedKVBucketTest::createDcpProducer( |
| 92 | + cookie, |
| 93 | + {}, |
| 94 | + /*dcpCollectionAware*/ false, |
| 95 | + IncludeDeleteTime::No); |
| 96 | + producer->scheduleCheckpointProcessorTask(); |
| 97 | + |
| 98 | + producer->mockActiveStreamRequest(consumerStream->getFlags(), |
| 99 | + consumerStream->getOpaque(), |
| 100 | + sourceVb, |
| 101 | + consumerStream->getStartSeqno(), |
| 102 | + consumerStream->getEndSeqno(), |
| 103 | + consumerStream->getVBucketUUID(), |
| 104 | + consumerStream->getSnapStartSeqno(), |
| 105 | + consumerStream->getSnapEndSeqno()); |
| 106 | + producerStream = dynamic_cast<MockActiveStream*>( |
| 107 | + producer->findStream(vbid).get()); |
| 108 | + |
| 109 | + // // Both streams created. Check state is as expected. |
| 110 | + ASSERT_TRUE(producerStream->isTakeoverSend()) |
| 111 | + << "Producer stream state should have transitioned to " |
| 112 | + "TakeoverSend"; |
| 113 | + ASSERT_EQ(2, sourceVb.checkpointManager->getNumOfCursors()) |
| 114 | + << "Should have both persistence and DCP producer cursor on " |
| 115 | + "source " |
| 116 | + "VB"; |
| 117 | + |
| 118 | + // Creating a producer will schedule one |
| 119 | + // ActiveStreamCheckpointProcessorTask |
| 120 | + // that task though sleeps forever, so won't run until woken. |
| 121 | + ASSERT_EQ(1, getLpAuxQ()->getFutureQueueSize()); |
| 122 | + } |
| 123 | + |
| 124 | + void TearDown() override { |
| 125 | + producer->cancelCheckpointCreatorTask(); |
| 126 | + producer->closeAllStreams(); |
| 127 | + producer.reset(); |
| 128 | + |
| 129 | + consumer->closeAllStreams(); |
| 130 | + consumer.reset(); |
| 131 | + shutdownAndPurgeTasks(replicaEngine.get()); |
| 132 | + destroy_mock_cookie(cookie); |
| 133 | + cookie = nullptr; |
| 134 | + replicaEngine.reset(); |
| 135 | + SingleThreadedKVBucketTest::TearDown(); |
| 136 | + } |
| 137 | + |
| 138 | + TaskQueue* getLpAuxQ() const { |
| 139 | + auto* task_executor = reinterpret_cast<SingleThreadedExecutorPool*>( |
| 140 | + ExecutorPool::get()); |
| 141 | + return task_executor->getLpTaskQ()[AUXIO_TASK_IDX]; |
| 142 | + } |
| 143 | + |
| 144 | + std::unique_ptr<DcpResponse> getNextProducerMsg(MockActiveStream* stream) { |
| 145 | + std::unique_ptr<DcpResponse> producerMsg(stream->next()); |
| 146 | + if (!producerMsg) { |
| 147 | + EXPECT_EQ(1, getLpAuxQ()->getFutureQueueSize()) |
| 148 | + << "Expected to have ActiveStreamCheckpointProcessorTask " |
| 149 | + "in future queue after null producerMsg"; |
| 150 | + stream->nextCheckpointItemTask(); |
| 151 | + EXPECT_GT(stream->getItemsRemaining(), 0) |
| 152 | + << "Expected some items ready after calling " |
| 153 | + "nextCheckpointItemTask()"; |
| 154 | + return getNextProducerMsg(stream); |
| 155 | + } |
| 156 | + return producerMsg; |
| 157 | + } |
| 158 | + |
| 159 | + void readNextConsumerMsgAndSendToProducer(ActiveStream& producerStream, |
| 160 | + PassiveStream& consumerStream); |
| 161 | + |
| 162 | + std::unique_ptr<SynchronousEPEngine> replicaEngine; |
| 163 | + std::shared_ptr<MockDcpConsumer> consumer; |
| 164 | + // Non-owning ptr to consumer stream (owned by consumer). |
| 165 | + PassiveStream* consumerStream; |
| 166 | + |
| 167 | + std::shared_ptr<MockDcpProducer> producer; |
| 168 | + |
| 169 | + // Non-owning ptr to producer stream (owned by producer). |
| 170 | + MockActiveStream* producerStream; |
| 171 | +}; |
| 172 | + |
| 173 | +void DCPLoopbackStreamTest::readNextConsumerMsgAndSendToProducer( |
| 174 | + ActiveStream& producerStream, PassiveStream& consumerStream) { |
| 175 | + std::unique_ptr<DcpResponse> consumerMsg(consumerStream.next()); |
| 176 | + |
| 177 | + // Pass the consumer's message to the producer. |
| 178 | + if (consumerMsg) { |
| 179 | + switch (consumerMsg->getEvent()) { |
| 180 | + case DcpResponse::Event::SnapshotMarker: |
| 181 | + producerStream.snapshotMarkerAckReceived(); |
| 182 | + break; |
| 183 | + case DcpResponse::Event::SetVbucket: |
| 184 | + producerStream.setVBucketStateAckRecieved(); |
| 185 | + break; |
| 186 | + default: |
| 187 | + FAIL(); |
| 188 | + } |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +/** |
| 193 | + * Test the behavour of a Takeover stream between a DcpProducer and DcpConsumer. |
| 194 | + * |
| 195 | + * Creates a Producer and Consumer; along with a single Active -> Passive |
| 196 | + * stream, then makes a streamRequest (simulating what ns_server normally does). |
| 197 | + * Then loops; reading messages from the producer and passing them to the |
| 198 | + * consumer, and reading responses from the consumer and passing to the |
| 199 | + * producer. Test finishes when the PassiveStream is set to Dead - at that point |
| 200 | + * the vBucket should be active on the destination; and dead on the source. |
| 201 | + */ |
| 202 | +TEST_F(DCPLoopbackStreamTest, Takeover) { |
| 203 | + while (true) { |
| 204 | + auto producerMsg = getNextProducerMsg(producerStream); |
| 205 | + |
| 206 | + // Pass the message onto the consumer. |
| 207 | + EXPECT_EQ(ENGINE_SUCCESS, |
| 208 | + consumerStream->messageReceived(std::move(producerMsg))); |
| 209 | + |
| 210 | + // Get the next message from the consumer; and pass to the producer. |
| 211 | + readNextConsumerMsgAndSendToProducer(*producerStream, *consumerStream); |
| 212 | + |
| 213 | + // Check consumer stream state - drop reflecting messages when |
| 214 | + // stream goes dead. |
| 215 | + if (!consumerStream->isActive()) { |
| 216 | + break; |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + auto* sourceVb = engine->getVBucket(vbid).get(); |
| 221 | + EXPECT_EQ(vbucket_state_dead, sourceVb->getState()) |
| 222 | + << "Expected producer vBucket to be dead once stream " |
| 223 | + "transitions to dead."; |
| 224 | + |
| 225 | + auto* destVb = replicaEngine->getVBucket(vbid).get(); |
| 226 | + EXPECT_EQ(vbucket_state_active, destVb->getState()) |
| 227 | + << "Expected consumer vBucket to be active once stream " |
| 228 | + "transitions to dead."; |
| 229 | +} |
0 commit comments