MB-24034: VBucketTest: addOne/setOne to return Add/MutationStatus
[ep-engine.git] / tests / module_tests / ephemeral_vb_test.cc
1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3  *     Copyright 2017 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  * Tests specific to Ephemeral VBuckets.
20  */
21
22 #include "config.h"
23
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"
29
30 #include <thread>
31
32 class EphemeralVBucketTest : public VBucketTest {
33 protected:
34     void SetUp() {
35         /* to test ephemeral vbucket specific stuff */
36         mockEpheVB = new MockEphemeralVBucket(0,
37                                               vbucket_state_active,
38                                               global_stats,
39                                               checkpoint_config,
40                                               /*kvshard*/ nullptr,
41                                               /*lastSeqno*/ 0,
42                                               /*lastSnapStart*/ 0,
43                                               /*lastSnapEnd*/ 0,
44                                               /*table*/ nullptr,
45                                               /*newSeqnoCb*/ nullptr,
46                                               config,
47                                               VALUE_ONLY);
48         /* vbucket manages the life time of mockEpheVB and is a base test class
49            ptr of owning type */
50         vbucket.reset(mockEpheVB);
51     }
52
53     void TearDown() {
54         vbucket.reset();
55     }
56
57     /* We want a ptr to MockEphemeralVBucket as we test ephemeral vbucket
58        specific stuff in this class */
59     MockEphemeralVBucket* mockEpheVB;
60
61     EPStats global_stats;
62     CheckpointConfig checkpoint_config;
63     Configuration config;
64 };
65
66 // Verify that attempting to pageOut an item twice has no effect the second
67 // time.
68 TEST_F(EphemeralVBucketTest, DoublePageOut) {
69     auto key = makeStoredDocKey("key");
70     ASSERT_EQ(AddStatus::Success, addOne(key));
71     ASSERT_EQ(1, vbucket->getNumItems());
72
73     auto lock_sv = lockAndFind(key);
74     auto* storedVal = lock_sv.second;
75     ASSERT_FALSE(storedVal->isDeleted());
76
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());
81
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());
86 }
87
88
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
93     // XATTR).
94     auto key = makeStoredDocKey("key");
95     std::string value = "deleted value";
96     Item item(key, 0, /*expiry*/0, value.data(), value.size());
97     item.setDeleted();
98     ASSERT_EQ(AddStatus::Success, public_processAdd(item));
99     ASSERT_EQ(0, vbucket->getNumItems());
100
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());
106
107     // Page it out.
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());
112 }
113
114 TEST_F(EphemeralVBucketTest, SetItems) {
115     const int numItems = 3;
116
117     auto keys = generateKeys(numItems);
118     setMany(keys, MutationStatus::WasClean);
119
120     EXPECT_EQ(numItems, vbucket->getNumItems());
121     EXPECT_EQ(numItems, vbucket->getHighSeqno());
122 }
123
124 TEST_F(EphemeralVBucketTest, UpdateItems) {
125     /* Add 3 items and then update all of them */
126     const int numItems = 3;
127
128     auto keys = generateKeys(numItems);
129     setMany(keys, MutationStatus::WasClean);
130
131     /* Update the items */
132     setMany(keys, MutationStatus::WasDirty);
133
134     EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
135     EXPECT_EQ(numItems, vbucket->getNumItems());
136 }
137
138 TEST_F(EphemeralVBucketTest, SoftDelete) {
139     /* Add 3 items and then delete all of them */
140     const int numItems = 3;
141
142     auto keys = generateKeys(numItems);
143     setMany(keys, MutationStatus::WasClean);
144
145     /* soft delete all */
146     softDeleteMany(keys, MutationStatus::WasDirty);
147
148     EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
149     EXPECT_EQ(0, vbucket->getNumItems());
150 }
151
152 TEST_F(EphemeralVBucketTest, Backfill) {
153     /* Add 3 items and get them by backfill */
154     const int numItems = 3;
155
156     auto keys = generateKeys(numItems);
157     setMany(keys, MutationStatus::WasClean);
158
159     auto res = mockEpheVB->inMemoryBackfill(1, numItems);
160     EXPECT_EQ(ENGINE_SUCCESS, res.first);
161     EXPECT_EQ(numItems, res.second.size());
162 }
163
164 TEST_F(EphemeralVBucketTest, UpdateDuringBackfill) {
165     /* Add 5 items and then update all of them */
166     const int numItems = 5;
167
168     auto keys = generateKeys(numItems);
169     setMany(keys, MutationStatus::WasClean);
170
171     /* Set up a mock backfill by setting the range of the backfill */
172     mockEpheVB->registerFakeReadRange(2, numItems - 1);
173
174     /* Update the first, middle and last item in the range read and 2 items
175        that are outside (before and after) range read */
176     ASSERT_EQ(MutationStatus::WasDirty, setOne(keys[0]));
177     for (int i = 1; i < numItems - 1; ++i) {
178         ASSERT_EQ(MutationStatus::WasClean, setOne(keys[i]));
179     }
180     ASSERT_EQ(MutationStatus::WasDirty, setOne(keys[numItems - 1]));
181
182     /* Hash table must have only recent (updated) items */
183     EXPECT_EQ(numItems, vbucket->getNumItems());
184
185     /* High Seqno must be 2 * numItems */
186     EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
187
188     /* LinkedList must have 3 stale items */
189     EXPECT_EQ(3, mockEpheVB->public_getNumStaleItems());
190
191     EXPECT_EQ(numItems * 2 - /* since 2 items are deduped*/ 2,
192               mockEpheVB->public_getNumListItems());
193 }
194
195 TEST_F(EphemeralVBucketTest, SoftDeleteDuringBackfill) {
196     /* Add 5 items and then soft delete all of them */
197     const int numItems = 5;
198
199     auto keys = generateKeys(numItems);
200     setMany(keys, MutationStatus::WasClean);
201
202     /* Set up a mock backfill by setting the range of the backfill */
203     mockEpheVB->registerFakeReadRange(2, numItems - 1);
204
205     /* Update the first, middle and last item in the range read and 2 items
206        that are outside (before and after) range read */
207     softDeleteMany(keys, MutationStatus::WasDirty);
208
209     /* Hash table must have only recent (updated) items */
210     EXPECT_EQ(0, vbucket->getNumItems());
211
212     /* High Seqno must be 2 * numItems */
213     EXPECT_EQ(numItems * 2, vbucket->getHighSeqno());
214
215     /* LinkedList must have 3 stale items */
216     EXPECT_EQ(3, mockEpheVB->public_getNumStaleItems());
217
218     EXPECT_EQ(numItems * 2 - /* since 2 items are deduped*/ 2,
219               mockEpheVB->public_getNumListItems());
220 }
221
222 // EphemeralVB Tombstone Purging //////////////////////////////////////////////
223
224 class EphTombstoneTest : public EphemeralVBucketTest {
225 protected:
226     void SetUp() override {
227         EphemeralVBucketTest::SetUp();
228
229         // Store three items to work with.
230         keys = generateKeys(3);
231         setMany(keys, MutationStatus::WasClean);
232         ASSERT_EQ(3, vbucket->getNumItems());
233     }
234     std::vector<StoredDocKey> keys;
235 };
236
237 // Check an empty seqList is handled correctly.
238 TEST_F(EphTombstoneTest, ZeroElementPurge) {
239     // Create a new empty VB (using parent class SetUp).
240     EphemeralVBucketTest::SetUp();
241     ASSERT_EQ(0, mockEpheVB->public_getNumListItems());
242
243     EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
244 }
245
246 // Check a seqList with one element is handled correctly.
247 TEST_F(EphTombstoneTest, OneElementPurge) {
248     // Create a new empty VB (using parent class SetUp).
249     EphemeralVBucketTest::SetUp();
250     ASSERT_EQ(MutationStatus::WasClean, setOne(makeStoredDocKey("one")));
251     ASSERT_EQ(1, mockEpheVB->public_getNumListItems());
252
253     EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
254 }
255
256 // Check that nothing is purged if no items are stale.
257 TEST_F(EphTombstoneTest, NoPurgeIfNoneStale) {
258     // Run purger - nothing should be removed.
259     EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
260     EXPECT_EQ(keys.size(), vbucket->getNumItems());
261 }
262
263 // Check that deletes are not purged if they are not old enough.
264 TEST_F(EphTombstoneTest, NoPurgeIfNoneOldEnough) {
265     // Delete the first item "now"
266     softDeleteOne(keys.at(0), MutationStatus::WasDirty);
267     ASSERT_EQ(2, vbucket->getNumItems());
268     ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
269
270     // Advance time by 5 seconds and run the EphTombstonePurger specifying a
271     // purge_age of 10s - nothing
272     // should be purged.
273     TimeTraveller theTerminator(5);
274     EXPECT_EQ(0, mockEpheVB->purgeTombstones(10));
275
276     EXPECT_EQ(2, vbucket->getNumItems());
277     EXPECT_EQ(1, vbucket->getNumInMemoryDeletes());
278 }
279
280 // Check that items should be purged when they are old enough.
281 TEST_F(EphTombstoneTest, OnePurgeIfDeletedItemOld) {
282     // Delete the first item "now"
283     softDeleteOne(keys.at(0), MutationStatus::WasDirty);
284     ASSERT_EQ(2, vbucket->getNumItems());
285     ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
286
287     // Delete the second item at time 30.
288     TimeTraveller looper(30);
289     softDeleteOne(keys.at(1), MutationStatus::WasDirty);
290     ASSERT_EQ(1, vbucket->getNumItems());
291     ASSERT_EQ(2, vbucket->getNumInMemoryDeletes());
292
293     // and the third at time 60.
294     TimeTraveller looper2(30);
295     softDeleteOne(keys.at(2), MutationStatus::WasDirty);
296     ASSERT_EQ(0, vbucket->getNumItems());
297     ASSERT_EQ(3, vbucket->getNumInMemoryDeletes());
298
299     // Run the EphTombstonePurger specifying a purge_age of 60s - only key0
300     // should be purged.
301     mockEpheVB->purgeTombstones(60);
302
303     EXPECT_EQ(0, vbucket->getNumItems());
304     EXPECT_EQ(2, vbucket->getNumInMemoryDeletes());
305     EXPECT_EQ(4, vbucket->getPurgeSeqno())
306             << "Should have purged up to 4th update (1st delete, after 3 sets)";
307     EXPECT_EQ(nullptr, findValue(keys.at(0)));
308     EXPECT_NE(nullptr, findValue(keys.at(1)));
309     EXPECT_NE(nullptr, findValue(keys.at(2)));
310 }
311
312 // Check that deleted items can be purged immediately.
313 TEST_F(EphTombstoneTest, ImmediateDeletedPurge) {
314     // Advance to non-zero time.
315     TimeTraveller jamesCole(10);
316
317     // Delete the first item at 10s
318     softDeleteOne(keys.at(0), MutationStatus::WasDirty);
319     ASSERT_EQ(2, vbucket->getNumItems());
320     ASSERT_EQ(1, vbucket->getNumInMemoryDeletes());
321
322     // Run the EphTombstonePurger specifying a purge_age of 0s - key0 should
323     // be immediately purged.
324     mockEpheVB->purgeTombstones(0);
325     EXPECT_EQ(2, vbucket->getNumItems());
326     EXPECT_EQ(0, vbucket->getNumInMemoryDeletes());
327     EXPECT_EQ(4, vbucket->getPurgeSeqno())
328             << "Should have purged up to 4th update (1st delete, after 3 sets)";
329     EXPECT_EQ(nullptr, findValue(keys.at(0)));
330     EXPECT_NE(nullptr, findValue(keys.at(1)));
331     EXPECT_NE(nullptr, findValue(keys.at(2)));
332 }
333
334 // Check that alive, stale items have no constraint on age.
335 TEST_F(EphTombstoneTest, ImmediatePurgeOfAliveStale) {
336     // Perform a mutation on the second element, with a (fake) Range Read in
337     // place; causing the initial OSV to be marked as stale and a new OSV to
338     // be added for that key.
339     auto& seqList = mockEpheVB->getLL()->getSeqList();
340     {
341         std::lock_guard<std::mutex> rrGuard(
342                 mockEpheVB->getLL()->getRangeReadLock());
343         mockEpheVB->registerFakeReadRange(1, 2);
344         ASSERT_EQ(MutationStatus::WasClean, setOne(keys.at(1)));
345
346         // Sanity check - our state is as expected:
347         ASSERT_EQ(3, vbucket->getNumItems());
348         ASSERT_EQ(4, seqList.size());
349         auto staleIt = std::next(seqList.begin());
350         auto newIt = seqList.rbegin();
351         ASSERT_EQ(staleIt->getKey(), newIt->getKey());
352         {
353             std::lock_guard<std::mutex> writeGuard(
354                     mockEpheVB->getLL()->getWriteLock());
355             ASSERT_TRUE(staleIt->isStale(writeGuard));
356             ASSERT_FALSE(newIt->isStale(writeGuard));
357         }
358
359         // Attempt a purge - should not remove anything as read range is in
360         // place.
361         EXPECT_EQ(0, mockEpheVB->purgeTombstones(0));
362         EXPECT_EQ(3, vbucket->getNumItems());
363         EXPECT_EQ(4, seqList.size());
364         EXPECT_EQ(0, vbucket->getPurgeSeqno());
365
366         // Clear the ReadRange (so we can actually purge items) and retry the
367         // purge which should now succeed.
368         mockEpheVB->getLL()->resetReadRange();
369     } // END rrGuard.
370
371     EXPECT_EQ(1, mockEpheVB->purgeTombstones(0));
372     EXPECT_EQ(3, vbucket->getNumItems());
373     EXPECT_EQ(3, seqList.size());
374 }
375
376 // Test that deleted items purged out of order are handled correctly (and
377 // highestDeletedPurged is updated).
378 TEST_F(EphTombstoneTest, PurgeOutOfOrder) {
379     // Delete the 3rd item.
380     softDeleteOne(keys.at(2), MutationStatus::WasDirty);
381
382     // Run the tombstone purger.
383     mockEpheVB->getLL()->resetReadRange();
384     ASSERT_EQ(1, mockEpheVB->purgeTombstones(0));
385     ASSERT_EQ(2, vbucket->getNumItems());
386     EXPECT_EQ(4, vbucket->getPurgeSeqno());
387
388     // Delete the 1st item
389     softDeleteOne(keys.at(0), MutationStatus::WasDirty);
390
391     // Run the tombstone purger. This should succeed, but with
392     // highestDeletedPurged unchanged.
393     ASSERT_EQ(1, mockEpheVB->purgeTombstones(0));
394     ASSERT_EQ(1, vbucket->getNumItems());
395     EXPECT_EQ(5, vbucket->getPurgeSeqno());
396 }
397
398 // Thread-safety test (intended to run via Valgrind / ASan / TSan) -
399 // perform sets and deletes on 2 additional threads while the purger
400 // runs constantly in the main thread.
401 TEST_F(EphTombstoneTest, ConcurrentPurge) {
402     ThreadGate started(2);
403     std::atomic<size_t> completed(0);
404
405     auto writer = [this](
406             ThreadGate& started, std::atomic<size_t>& completed, size_t id) {
407         started.threadUp();
408         for (size_t ii = 0; ii < 5000; ++ii) {
409             auto key = makeStoredDocKey(std::to_string(id) + ":key_" +
410                                         std::to_string(ii));
411             Item item(key, /*flags*/ 0, /*expiry*/ 0, key.data(), key.size());
412             public_processSet(item, item.getCas());
413             softDeleteOne(key, MutationStatus::WasDirty);
414         }
415         ++completed;
416     };
417     std::thread fe1{writer, std::ref(started), std::ref(completed), 1};
418     std::thread fe2{writer, std::ref(started), std::ref(completed), 2};
419
420     size_t purged = 0;
421     do {
422         purged += mockEpheVB->purgeTombstones(0);
423     } while (completed != 2);
424
425     fe1.join();
426     fe2.join();
427 }
428
429 // Test that on a double-delete (delete with a different value) the deleted time
430 // is updated correctly.
431 TEST_F(EphTombstoneTest, DoubleDeleteTimeCorrect) {
432     // Delete the first item at +0s
433     auto key = keys.at(0);
434     softDeleteOne(key, MutationStatus::WasDirty);
435     auto* delOSV = findValue(key)->toOrderedStoredValue();
436     auto initialDelTime = delOSV->getDeletedTime();
437     ASSERT_EQ(2, delOSV->getRevSeqno()) << "Should be initial set + 1";
438
439     // Advance to non-zero time.
440     const int timeJump = 10;
441     TimeTraveller nonZero(timeJump);
442     ASSERT_GE(ep_current_time(), initialDelTime + timeJump)
443             << "Failed to advance at least " + std::to_string(timeJump) +
444                        " seconds from when initial delete "
445                        "occcured";
446
447     // Delete the same key again (delete-with-Value), checking the deleted
448     // time has changed.
449     Item item(key, 0, 0, "deleted", strlen("deleted"));
450     item.setDeleted();
451     ASSERT_EQ(MutationStatus::WasDirty, public_processSet(item, item.getCas()));
452     ASSERT_EQ(3, delOSV->getRevSeqno()) << "Should be initial set + 2";
453
454     auto secondDelTime = delOSV->getDeletedTime();
455     EXPECT_GE(secondDelTime, initialDelTime + timeJump);
456 }