1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
3 * Copyright 2017 Couchbase, Inc
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * Tests specific to Ephemeral VBuckets.
24 #include "../mock/mock_ephemeral_vb.h"
25 #include "failover-table.h"
26 #include "test_helpers.h"
27 #include "thread_gate.h"
28 #include "vbucket_test.h"
32 class EphemeralVBucketTest : public VBucketTest {
35 /* to test ephemeral vbucket specific stuff */
36 mockEpheVB = new MockEphemeralVBucket(0,
45 /*newSeqnoCb*/ nullptr,
48 /* vbucket manages the life time of mockEpheVB and is a base test class
50 vbucket.reset(mockEpheVB);
57 /* We want a ptr to MockEphemeralVBucket as we test ephemeral vbucket
58 specific stuff in this class */
59 MockEphemeralVBucket* mockEpheVB;
62 CheckpointConfig checkpoint_config;
66 // Verify that attempting to pageOut an item twice has no effect the second
68 TEST_F(EphemeralVBucketTest, DoublePageOut) {
69 auto key = makeStoredDocKey("key");
70 ASSERT_EQ(AddStatus::Success, addOne(key));
71 ASSERT_EQ(1, vbucket->getNumItems());
73 auto lock_sv = lockAndFind(key);
74 auto* storedVal = lock_sv.second;
75 ASSERT_FALSE(storedVal->isDeleted());
77 // Page out the item (once).
78 EXPECT_TRUE(vbucket->pageOut(lock_sv.first, storedVal));
79 EXPECT_EQ(0, vbucket->getNumItems());
80 EXPECT_TRUE(storedVal->isDeleted());
82 // Attempt to page out again - should not be possible.
83 EXPECT_FALSE(vbucket->pageOut(lock_sv.first, storedVal));
84 EXPECT_EQ(0, vbucket->getNumItems());
85 EXPECT_TRUE(storedVal->isDeleted());
89 // Verify that we can pageOut deleted items which have a value associated with
90 // them - and afterwards the value is null.
91 TEST_F(EphemeralVBucketTest, PageOutAfterDeleteWithValue) {
92 // Add an item which is marked as deleted, but has a body (e.g. system
94 auto key = makeStoredDocKey("key");
95 std::string value = "deleted value";
96 Item item(key, 0, /*expiry*/0, value.data(), value.size());
98 ASSERT_EQ(AddStatus::Success, public_processAdd(item));
99 ASSERT_EQ(0, vbucket->getNumItems());
101 // Check preconditions
102 auto lock_sv = lockAndFind(key);
103 auto* storedVal = lock_sv.second;
104 ASSERT_TRUE(storedVal->isDeleted());
105 ASSERT_EQ(value, storedVal->getValue()->to_s());
108 EXPECT_TRUE(vbucket->pageOut(lock_sv.first, storedVal));
109 EXPECT_EQ(0, vbucket->getNumItems());
110 EXPECT_TRUE(storedVal->isDeleted());
111 EXPECT_FALSE(storedVal->getValue());
114 // NRU: check the seqlist has correct statistics for a create, pageout,
115 // and (re)create of the same key.
116 TEST_F(EphemeralVBucketTest, CreatePageoutCreate) {
117 auto key = makeStoredDocKey("key");
119 // Add a key, then page out.
120 ASSERT_EQ(AddStatus::Success, addOne(key));
122 auto lock_sv = lockAndFind(key);
123 EXPECT_TRUE(vbucket->pageOut(lock_sv.first, lock_sv.second));
125 // Sanity check - should have just the one deleted item.
126 ASSERT_EQ(0, vbucket->getNumItems());
127 ASSERT_EQ(1, mockEpheVB->getLL()->getNumDeletedItems());
129 // Test: Set the key again.
130 ASSERT_EQ(MutationStatus::WasDirty, setOne(key));
132 EXPECT_EQ(1, vbucket->getNumItems());
133 EXPECT_EQ(0, mockEpheVB->getLL()->getNumDeletedItems());
135 // Finally for good measure, delete again and check the numbers are correct.
137 auto lock_sv = lockAndFind(key);
138 EXPECT_TRUE(vbucket->pageOut(lock_sv.first, lock_sv.second));
140 EXPECT_EQ(0, vbucket->getNumItems());
141 EXPECT_EQ(1, mockEpheVB->getLL()->getNumDeletedItems());
144 TEST_F(EphemeralVBucketTest, SetItems) {
145 const int numItems = 3;
147 auto keys = generateKeys(numItems);
148 setMany(keys, MutationStatus::WasClean);
150 EXPECT_EQ(numItems, vbucket->getNumItems());
151 EXPECT_EQ(numItems, vbucket->getHighSeqno());
154 TEST_F(EphemeralVBucketTest, UpdateItems) {
155 /* Add 3 items and then update all of them */
156 const int numItems = 3;
158 auto keys = generateKeys(numItems);
159 setMany(keys, MutationStatus::WasClean);
161 /* Update the items */
162 setMany(keys, MutationStatus::WasDirty);
164 EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
165 EXPECT_EQ(numItems, vbucket->getNumItems());
168 TEST_F(EphemeralVBucketTest, SoftDelete) {
169 /* Add 3 items and then delete all of them */
170 const int numItems = 3;
172 auto keys = generateKeys(numItems);
173 setMany(keys, MutationStatus::WasClean);
175 /* soft delete all */
176 softDeleteMany(keys, MutationStatus::WasDirty);
178 EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
179 EXPECT_EQ(0, vbucket->getNumItems());
182 TEST_F(EphemeralVBucketTest, Backfill) {
183 /* Add 3 items and get them by backfill */
184 const int numItems = 3;
186 auto keys = generateKeys(numItems);
187 setMany(keys, MutationStatus::WasClean);
189 auto res = mockEpheVB->inMemoryBackfill(1, numItems);
190 EXPECT_EQ(ENGINE_SUCCESS, res.first);
191 EXPECT_EQ(numItems, res.second.size());
194 TEST_F(EphemeralVBucketTest, UpdateDuringBackfill) {
195 /* Add 5 items and then update all of them */
196 const int numItems = 5;
198 auto keys = generateKeys(numItems);
199 setMany(keys, MutationStatus::WasClean);
201 /* Set up a mock backfill by setting the range of the backfill */
202 mockEpheVB->registerFakeReadRange(2, numItems - 1);
204 /* Update the first, middle and last item in the range read and 2 items
205 that are outside (before and after) range read */
206 ASSERT_EQ(MutationStatus::WasDirty, setOne(keys[0]));
207 for (int i = 1; i < numItems - 1; ++i) {
208 ASSERT_EQ(MutationStatus::WasClean, setOne(keys[i]));
210 ASSERT_EQ(MutationStatus::WasDirty, setOne(keys[numItems - 1]));
212 /* Hash table must have only recent (updated) items */
213 EXPECT_EQ(numItems, vbucket->getNumItems());
215 /* High Seqno must be 2 * numItems */
216 EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
218 /* LinkedList must have 3 stale items */
219 EXPECT_EQ(3, mockEpheVB->public_getNumStaleItems());
221 EXPECT_EQ(numItems * 2 - /* since 2 items are deduped*/ 2,
222 mockEpheVB->public_getNumListItems());
225 TEST_F(EphemeralVBucketTest, SoftDeleteDuringBackfill) {
226 /* Add 5 items and then soft delete all of them */
227 const int numItems = 5;
229 auto keys = generateKeys(numItems);
230 setMany(keys, MutationStatus::WasClean);
232 /* Set up a mock backfill by setting the range of the backfill */
233 mockEpheVB->registerFakeReadRange(2, numItems - 1);
235 /* Update the first, middle and last item in the range read and 2 items
236 that are outside (before and after) range read */
237 softDeleteMany(keys, MutationStatus::WasDirty);
239 /* Hash table must have only recent (updated) items */
240 EXPECT_EQ(0, vbucket->getNumItems());
242 /* High Seqno must be 2 * numItems */
243 EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
245 /* LinkedList must have 3 stale items */
246 EXPECT_EQ(3, mockEpheVB->public_getNumStaleItems());
248 EXPECT_EQ(numItems * 2 - /* since 2 items are deduped*/ 2,
249 mockEpheVB->public_getNumListItems());
252 // EphemeralVB Tombstone Purging //////////////////////////////////////////////
254 class EphTombstoneTest : public EphemeralVBucketTest {
256 void SetUp() override {
257 EphemeralVBucketTest::SetUp();
259 // Store three items to work with.
260 keys = generateKeys(3);
261 setMany(keys, MutationStatus::WasClean);
262 ASSERT_EQ(3, vbucket->getNumItems());
264 std::vector<StoredDocKey> keys;
267 // Check an empty seqList is handled correctly.
268 TEST_F(EphTombstoneTest, ZeroElementPurge) {
269 // Create a new empty VB (using parent class SetUp).
270 EphemeralVBucketTest::SetUp();
271 ASSERT_EQ(0, mockEpheVB->public_getNumListItems());
273 EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
276 // Check a seqList with one element is handled correctly.
277 TEST_F(EphTombstoneTest, OneElementPurge) {
278 // Create a new empty VB (using parent class SetUp).
279 EphemeralVBucketTest::SetUp();
280 ASSERT_EQ(MutationStatus::WasClean, setOne(makeStoredDocKey("one")));
281 ASSERT_EQ(1, mockEpheVB->public_getNumListItems());
283 EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
286 // Check that nothing is purged if no items are stale.
287 TEST_F(EphTombstoneTest, NoPurgeIfNoneStale) {
288 // Run purger - nothing should be removed.
289 EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
290 EXPECT_EQ(keys.size(), vbucket->getNumItems());
293 // Check that deletes are not purged if they are not old enough.
294 TEST_F(EphTombstoneTest, NoPurgeIfNoneOldEnough) {
295 // Delete the first item "now"
296 softDeleteOne(keys.at(0), MutationStatus::WasDirty);
297 ASSERT_EQ(2, vbucket->getNumItems());
298 ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
300 // Advance time by 5 seconds and run the EphTombstonePurger specifying a
301 // purge_age of 10s - nothing
303 TimeTraveller theTerminator(5);
304 EXPECT_EQ(0, mockEpheVB->purgeTombstones(10));
306 EXPECT_EQ(2, vbucket->getNumItems());
307 EXPECT_EQ(1, vbucket->getNumInMemoryDeletes());
310 // Check that items should be purged when they are old enough.
311 TEST_F(EphTombstoneTest, OnePurgeIfDeletedItemOld) {
312 // Delete the first item "now"
313 softDeleteOne(keys.at(0), MutationStatus::WasDirty);
314 ASSERT_EQ(2, vbucket->getNumItems());
315 ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
317 // Delete the second item at time 30.
318 TimeTraveller looper(30);
319 softDeleteOne(keys.at(1), MutationStatus::WasDirty);
320 ASSERT_EQ(1, vbucket->getNumItems());
321 ASSERT_EQ(2, vbucket->getNumInMemoryDeletes());
323 // and the third at time 60.
324 TimeTraveller looper2(30);
325 softDeleteOne(keys.at(2), MutationStatus::WasDirty);
326 ASSERT_EQ(0, vbucket->getNumItems());
327 ASSERT_EQ(3, vbucket->getNumInMemoryDeletes());
329 // Run the EphTombstonePurger specifying a purge_age of 60s - only key0
331 mockEpheVB->purgeTombstones(60);
333 EXPECT_EQ(0, vbucket->getNumItems());
334 EXPECT_EQ(2, vbucket->getNumInMemoryDeletes());
335 EXPECT_EQ(4, vbucket->getPurgeSeqno())
336 << "Should have purged up to 4th update (1st delete, after 3 sets)";
337 EXPECT_EQ(nullptr, findValue(keys.at(0)));
338 EXPECT_NE(nullptr, findValue(keys.at(1)));
339 EXPECT_NE(nullptr, findValue(keys.at(2)));
342 // Check that deleted items can be purged immediately.
343 TEST_F(EphTombstoneTest, ImmediateDeletedPurge) {
344 // Advance to non-zero time.
345 TimeTraveller jamesCole(10);
347 // Delete the first item at 10s
348 softDeleteOne(keys.at(0), MutationStatus::WasDirty);
349 ASSERT_EQ(2, vbucket->getNumItems());
350 ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
352 // Run the EphTombstonePurger specifying a purge_age of 0s - key0 should
353 // be immediately purged.
354 mockEpheVB->purgeTombstones(0);
355 EXPECT_EQ(2, vbucket->getNumItems());
356 EXPECT_EQ(0, vbucket->getNumInMemoryDeletes());
357 EXPECT_EQ(4, vbucket->getPurgeSeqno())
358 << "Should have purged up to 4th update (1st delete, after 3 sets)";
359 EXPECT_EQ(nullptr, findValue(keys.at(0)));
360 EXPECT_NE(nullptr, findValue(keys.at(1)));
361 EXPECT_NE(nullptr, findValue(keys.at(2)));
364 // Check that alive, stale items have no constraint on age.
365 TEST_F(EphTombstoneTest, ImmediatePurgeOfAliveStale) {
366 // Perform a mutation on the second element, with a (fake) Range Read in
367 // place; causing the initial OSV to be marked as stale and a new OSV to
368 // be added for that key.
369 auto& seqList = mockEpheVB->getLL()->getSeqList();
371 std::lock_guard<std::mutex> rrGuard(
372 mockEpheVB->getLL()->getRangeReadLock());
373 mockEpheVB->registerFakeReadRange(1, 2);
374 ASSERT_EQ(MutationStatus::WasClean, setOne(keys.at(1)));
376 // Sanity check - our state is as expected:
377 ASSERT_EQ(3, vbucket->getNumItems());
378 ASSERT_EQ(4, seqList.size());
379 auto staleIt = std::next(seqList.begin());
380 auto newIt = seqList.rbegin();
381 ASSERT_EQ(staleIt->getKey(), newIt->getKey());
383 std::lock_guard<std::mutex> writeGuard(
384 mockEpheVB->getLL()->getWriteLock());
385 ASSERT_TRUE(staleIt->isStale(writeGuard));
386 ASSERT_FALSE(newIt->isStale(writeGuard));
389 // Attempt a purge - should not remove anything as read range is in
391 EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
392 EXPECT_EQ(3, vbucket->getNumItems());
393 EXPECT_EQ(4, seqList.size());
394 EXPECT_EQ(0, vbucket->getPurgeSeqno());
396 // Clear the ReadRange (so we can actually purge items) and retry the
397 // purge which should now succeed.
398 mockEpheVB->getLL()->resetReadRange();
401 EXPECT_EQ(1, mockEpheVB->purgeTombstones(0));
402 EXPECT_EQ(3, vbucket->getNumItems());
403 EXPECT_EQ(3, seqList.size());
406 // Test that deleted items purged out of order are handled correctly (and
407 // highestDeletedPurged is updated).
408 TEST_F(EphTombstoneTest, PurgeOutOfOrder) {
409 // Delete the 3rd item.
410 softDeleteOne(keys.at(2), MutationStatus::WasDirty);
412 // Run the tombstone purger.
413 mockEpheVB->getLL()->resetReadRange();
414 ASSERT_EQ(1, mockEpheVB->purgeTombstones(0));
415 ASSERT_EQ(2, vbucket->getNumItems());
416 EXPECT_EQ(4, vbucket->getPurgeSeqno());
418 // Delete the 1st item
419 softDeleteOne(keys.at(0), MutationStatus::WasDirty);
421 // Run the tombstone purger. This should succeed, but with
422 // highestDeletedPurged unchanged.
423 ASSERT_EQ(1, mockEpheVB->purgeTombstones(0));
424 ASSERT_EQ(1, vbucket->getNumItems());
425 EXPECT_EQ(5, vbucket->getPurgeSeqno());
428 // Thread-safety test (intended to run via Valgrind / ASan / TSan) -
429 // perform sets and deletes on 2 additional threads while the purger
430 // runs constantly in the main thread.
431 TEST_F(EphTombstoneTest, ConcurrentPurge) {
432 ThreadGate started(2);
433 std::atomic<size_t> completed(0);
435 auto writer = [this](
436 ThreadGate& started, std::atomic<size_t>& completed, size_t id) {
438 for (size_t ii = 0; ii < 5000; ++ii) {
439 auto key = makeStoredDocKey(std::to_string(id) + ":key_" +
441 Item item(key, /*flags*/ 0, /*expiry*/ 0, key.data(), key.size());
442 public_processSet(item, item.getCas());
443 softDeleteOne(key, MutationStatus::WasDirty);
447 std::thread fe1{writer, std::ref(started), std::ref(completed), 1};
448 std::thread fe2{writer, std::ref(started), std::ref(completed), 2};
452 purged += mockEpheVB->purgeTombstones(0);
453 } while (completed != 2);
459 // Test that on a double-delete (delete with a different value) the deleted time
460 // is updated correctly.
461 TEST_F(EphTombstoneTest, DoubleDeleteTimeCorrect) {
462 // Delete the first item at +0s
463 auto key = keys.at(0);
464 softDeleteOne(key, MutationStatus::WasDirty);
465 auto* delOSV = findValue(key)->toOrderedStoredValue();
466 auto initialDelTime = delOSV->getDeletedTime();
467 ASSERT_EQ(2, delOSV->getRevSeqno()) << "Should be initial set + 1";
469 // Advance to non-zero time.
470 const int timeJump = 10;
471 TimeTraveller nonZero(timeJump);
472 ASSERT_GE(ep_current_time(), initialDelTime + timeJump)
473 << "Failed to advance at least " + std::to_string(timeJump) +
474 " seconds from when initial delete "
477 // Delete the same key again (delete-with-Value), checking the deleted
479 Item item(key, 0, 0, "deleted", strlen("deleted"));
481 ASSERT_EQ(MutationStatus::WasDirty, public_processSet(item, item.getCas()));
482 ASSERT_EQ(3, delOSV->getRevSeqno()) << "Should be initial set + 2";
484 auto secondDelTime = delOSV->getDeletedTime();
485 EXPECT_GE(secondDelTime, initialDelTime + timeJump);