1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3  *     Copyright 2016 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 #include <cctype>
18 #include <limits>
19 #include <thread>
20 #include "testapp_arithmetic.h"
21 
22 INSTANTIATE_TEST_CASE_P(
23         TransportProtocols,
24         ArithmeticTest,
25         ::testing::Combine(::testing::Values(TransportProtocols::McbpPlain,
26                                              TransportProtocols::McbpIpv6Plain,
27                                              TransportProtocols::McbpSsl,
28                                              TransportProtocols::McbpIpv6Ssl),
29                            ::testing::Values(XattrSupport::Yes,
30                                              XattrSupport::No),
31                            ::testing::Values(ClientJSONSupport::Yes,
32                                              ClientJSONSupport::No),
33                            ::testing::Values(ClientSnappySupport::Yes,
34                                              ClientSnappySupport::No)),
35         PrintToStringCombinedName());
36 
37 // Tests which always need XATTR support (hence only instantiated with
38 // XattrSupport::Yes).
39 INSTANTIATE_TEST_CASE_P(
40         TransportProtocols,
41         ArithmeticXattrOnTest,
42         ::testing::Combine(::testing::Values(TransportProtocols::McbpPlain,
43                                              TransportProtocols::McbpIpv6Plain,
44                                              TransportProtocols::McbpSsl,
45                                              TransportProtocols::McbpIpv6Ssl),
46                            ::testing::Values(XattrSupport::Yes),
47                            ::testing::Values(ClientJSONSupport::Yes,
48                                              ClientJSONSupport::No),
49                            ::testing::Values(ClientSnappySupport::Yes,
50                                              ClientSnappySupport::No)),
51         PrintToStringCombinedName());
52 
TEST_P(ArithmeticTest, TestArithmeticNoCreateOnNotFound)53 TEST_P(ArithmeticTest, TestArithmeticNoCreateOnNotFound) {
54     auto& connection = getConnection();
55 
56     try {
57         connection.increment(name, 1, 0, 0xffffffff);
58         FAIL() << "Document should not be created";
59     } catch (const ConnectionError& error) {
60         EXPECT_TRUE(error.isNotFound()) << error.getReason();
61     }
62 
63     try {
64         connection.decrement(name, 1, 0, 0xffffffff);
65         FAIL() << "Document should not be created";
66     } catch (const ConnectionError& error) {
67         EXPECT_TRUE(error.isNotFound()) << error.getReason();
68     }
69 }
70 
TEST_P(ArithmeticTest, TestIncrementCreateOnNotFound)71 TEST_P(ArithmeticTest, TestIncrementCreateOnNotFound) {
72     auto& connection = getConnection();
73     EXPECT_EQ(0, connection.increment(name+"_incr", 1, 0));
74     EXPECT_EQ(0, connection.increment(name+"_decr", 1, 0));
75 }
76 
incr_decr_loop(MemcachedConnection& connection, const std::string& key, int delta)77 static void incr_decr_loop(MemcachedConnection& connection,
78                            const std::string& key,
79                            int delta) {
80 
81     uint64_t expected = 0;
82 
83     for (int ii = 0; ii < 101; ++ii) {
84         EXPECT_EQ(expected, connection.arithmetic(key, delta));
85         expected += delta;
86     }
87     // (remove the last delta, it was not added to the one on the server
88     expected -= delta;
89 
90     for (int ii = 0; ii < 100; ++ii) {
91         expected -= delta;
92         EXPECT_EQ(expected, connection.arithmetic(key, delta * -1));
93     }
94 
95     // Verify that we ended back at 0 by trying to add 0 ;-)
96     EXPECT_EQ(0, connection.arithmetic(key, 0));
97 }
98 
TEST_P(ArithmeticTest, TestBasicArithmetic_1)99 TEST_P(ArithmeticTest, TestBasicArithmetic_1) {
100     incr_decr_loop(getConnection(), name, 1);
101 }
102 
TEST_P(ArithmeticTest, TestBasicArithmetic_33)103 TEST_P(ArithmeticTest, TestBasicArithmetic_33) {
104     incr_decr_loop(getConnection(), name, 33);
105 }
106 
TEST_P(ArithmeticTest, TestDecrementDontWrap)107 TEST_P(ArithmeticTest, TestDecrementDontWrap) {
108     auto& connection = getConnection();
109     for (int ii = 0; ii < 10; ++ii) {
110         EXPECT_EQ(0, connection.decrement(name, 1));
111     }
112 }
113 
TEST_P(ArithmeticTest, TestIncrementDoesWrap)114 TEST_P(ArithmeticTest, TestIncrementDoesWrap) {
115     auto& connection = getConnection();
116     uint64_t initial = std::numeric_limits<uint64_t>::max();
117 
118     // Create the initial value so that we know where we should start off ;)
119     EXPECT_EQ(0, connection.increment(name, 1));
120 
121     EXPECT_EQ(initial, connection.increment(name, initial));
122     EXPECT_EQ(0, connection.increment(name, 1));
123     EXPECT_EQ(initial, connection.increment(name, initial));
124     EXPECT_EQ(0, connection.increment(name, 1));
125 }
126 
TEST_P(ArithmeticTest, TestConcurrentAccess)127 TEST_P(ArithmeticTest, TestConcurrentAccess) {
128     auto& conn = getConnection();
129     auto conn1 = conn.clone();
130     auto conn2 = conn.clone();
131     const int iterationCount = 100;
132     const int incrDelta = 7;
133     const int decrDelta = -3;
134 
135     // Create the starting point
136     uint64_t expected = std::numeric_limits<uint32_t>::max();
137     ASSERT_EQ(0, conn.increment(name, 0));
138     ASSERT_EQ(expected, conn.increment(name, expected));
139 
140     std::string doc = name;
141 
142     std::thread t1 {
143         [&conn1, &doc, &iterationCount, incrDelta]() {
144             conn1->arithmetic(doc + "_t1", 0, 0);
145 
146             // wait for the other thread to start
147             bool running = false;
148             while (!running) {
149                 try {
150                     conn1->arithmetic(doc + "_t2", 1, 0, 0xffffffff);
151                     running = true;
152                 } catch (const ConnectionError& error) {
153                     ASSERT_TRUE(error.isNotFound());
154                 }
155             }
156 
157             for (int ii = 0; ii < iterationCount; ++ii) {
158                 conn1->arithmetic(doc, incrDelta);
159             }
160         }
161     };
162 
163     std::thread t2 {
164         [&conn2, &doc, &iterationCount, &decrDelta]() {
165             conn2->arithmetic(doc + "_t2", 0, 0);
166             bool running = false;
167             while (!running) {
168                 try {
169                     conn2->arithmetic(doc + "_t1", 1, 0, 0xffffffff);
170                     running = true;
171                 } catch (const ConnectionError& error) {
172                     ASSERT_TRUE(error.isNotFound());
173                 }
174             }
175 
176             for (int ii = 0; ii < iterationCount; ++ii) {
177                 conn2->arithmetic(doc, decrDelta);
178             }
179         }
180     };
181 
182     // Wait for them to complete
183     t1.join();
184     t2.join();
185 
186     expected += (iterationCount * incrDelta) + (iterationCount * decrDelta);
187     EXPECT_EQ(expected, conn.increment(name, 0));
188 }
189 
TEST_P(ArithmeticTest, TestMutationInfo)190 TEST_P(ArithmeticTest, TestMutationInfo) {
191     auto& conn = getConnection();
192     MutationInfo info;
193     ASSERT_EQ(0, conn.increment(name, 0, 0, 0, &info));
194 
195     // Not all the backends supports the vbucket seqno and uuid..
196     // The cas should be filled out, so we should be able to do a CAS replace
197     Document doc;
198     doc.info.cas = info.cas;
199     doc.info.flags = 0xcaffee;
200     doc.info.id = name;
201     doc.value = to_string(memcached_cfg.get());
202 
203     conn.mutate(doc, 0, MutationType::Replace);
204 }
205 
TEST_P(ArithmeticTest, TestIllegalDatatype)206 TEST_P(ArithmeticTest, TestIllegalDatatype) {
207     auto& conn = getConnection();
208 
209     Document doc;
210     doc.info.cas = mcbp::cas::Wildcard;
211     doc.info.flags = 0xcaffee;
212     doc.info.id = name;
213     doc.value = to_string(memcached_cfg.get());
214 
215     ASSERT_NO_THROW(conn.mutate(doc, 0, MutationType::Add));
216 
217     try {
218         conn.increment(name, 0);
219     } catch (const ConnectionError& error) {
220         EXPECT_TRUE(error.isDeltaBadval()) << error.getReason();
221     }
222 }
223 
224 /*
225  * Unit test to test the fix for MB-33813
226  * This test checks that we do not return NOT_STORED back to the client
227  * if bucket_store returns it to ArithmeticCommandContext::storeNewItem().
228  * Instead ArithmeticCommandContext::storeNewItem() should reset the
229  * ArithmeticCommandContext state machine and try again.
230  *
231  * To check that ENGINE_NOT_STORED/NOT_STORED is not returned we use
232  * eWouldBlockEngine to return ENGINE_NOT_STORED on the 3rd engine request using
233  * the binary sequence 0b100.
234  *
235  * This works as the process we expect to see by the ArithmeticCommandContext
236  * state machine is as follows:
237  *  start: GetItem
238  *      calls bucket_get() should return ENGINE_SUCCESS
239  *  -> CreateNewItem
240  *      calls bucket_allocate_ex should return ENGINE_SUCCESS
241  *  -> StoreNewItem
242  *      calls bucket_store return ENGINE_NOT_STORED
243  *      ENGINE_NOT_STORED returned so reset and try again.
244  *  -> Reset
245  *  -> GetItem
246  *  .... Do the work to increment the value again
247  *  -> Done
248  */
TEST_P(ArithmeticTest, MB33813)249 TEST_P(ArithmeticTest, MB33813) {
250     auto& connection = getConnection();
251     std::string key(name + "_inc");
252 
253     // Make the 3rd request send to the engine return ENGINE_NOT_STORED
254     // In this case this will be the store that happens as a result of
255     // a call to ArithmeticCommandContext::storeNewItem()
256     // We also set all the higher bits after the 6th. So that if the sequence
257     // of engine accesses change this test will fail.
258     connection.configureEwouldBlockEngine(
259             EWBEngineMode::Sequence,
260             ENGINE_NOT_STORED,
261             /* 0b11111111111111111111111111000100 */ 0xffffffc4);
262 
263     EXPECT_EQ(1, connection.increment(key, 0, 1));
264 
265     connection.disableEwouldBlockEngine();
266 
267     Document doc = connection.get(key, 0);
268     EXPECT_EQ("1", doc.value);
269 
270     EXPECT_EQ(2, connection.increment(key, 1));
271 
272     doc = connection.get(key, 0);
273     EXPECT_EQ("2", doc.value);
274 
275     // Sanity check do the same thing but with a decrement
276     key = name + "_dec";
277     // Make the 3rd request send to the engine return ENGINE_NOT_STORED
278     // In this case this will be the store that happens as a result of
279     // a call to ArithmeticCommandContext::storeNewItem()
280     // We also set all the higher bits after the 6th. So that if the sequence
281     // of engine accesses change this test will fail.
282     connection.configureEwouldBlockEngine(
283             EWBEngineMode::Sequence,
284             ENGINE_NOT_STORED,
285             /* 0b11111111111111111111111111000100 */ 0xffffffc4);
286 
287     EXPECT_EQ(2, connection.decrement(key, 0, 2));
288 
289     connection.disableEwouldBlockEngine();
290 
291     doc = connection.get(key, 0);
292     EXPECT_EQ("2", doc.value);
293 
294     EXPECT_EQ(1, connection.decrement(key, 1));
295 
296     doc = connection.get(key, 0);
297     EXPECT_EQ("1", doc.value);
298 }
299 
test_stored_doc(MemcachedConnection& conn, const std::string& key, const std::string& content, bool badval)300 static void test_stored_doc(MemcachedConnection& conn,
301                             const std::string& key,
302                             const std::string& content,
303                             bool badval) {
304 
305     Document doc;
306     doc.info.cas = mcbp::cas::Wildcard;
307     doc.info.flags = 0xcaffee;
308     doc.info.id = key;
309     doc.value = content;
310 
311     ASSERT_NO_THROW(conn.mutate(doc, 0, MutationType::Set));
312     if (badval) {
313         try {
314             conn.increment(key, 1);
315             FAIL() << "Did not catch: " << content;
316         } catch (const ConnectionError& error) {
317             EXPECT_TRUE(error.isDeltaBadval()) << error.getReason();
318         }
319     } else {
320         EXPECT_EQ(1, conn.increment(key, 1));
321     }
322 }
323 
TEST_P(ArithmeticTest, TestOperateOnStoredDocument)324 TEST_P(ArithmeticTest, TestOperateOnStoredDocument) {
325 #ifdef THREAD_SANITIZER
326     // This test takes ~20 secs under Thread sanitizer, and it is
327     // mostly here in order to validate that we correctly detect if
328     // we can perform incr/decr on a document stored in the cache
329     // (depending on the content being a legal value or not)
330     // To speed up CV, just run the test for a single parameterization.
331     if (GetParam() != std::make_tuple(TransportProtocols::McbpPlain,
332                                       XattrSupport::Yes,
333                                       ClientJSONSupport::Yes,
334                                       ClientSnappySupport::Yes)) {
335         return;
336     }
337 #endif
338     auto& conn = getConnection();
339 
340     // It is "allowed" for the value to be padded with whitespace
341     // before and after the numeric value.
342     // but no other characters should be present!
343     for (unsigned int ii = 0; ii < 256; ++ii) {
344         if (std::isspace(ii)) {
345             std::string content;
346             content = std::string{"0"} + char(ii);
347             test_stored_doc(conn, name, content, false);
348             content = char(ii) + std::string{"0"};
349             test_stored_doc(conn, name, content, false);
350         } else if (!std::isdigit(ii)) {
351             std::string content;
352             if (ii != 0) {
353                 content = std::string{"0"} + char(ii);
354                 test_stored_doc(conn, name, content, true);
355             }
356 
357             if (ii != '-' && ii != '+') {
358                 content = char(ii) + std::string{"0"};
359                 test_stored_doc(conn, name, content, true);
360             }
361         }
362     }
363 }
364 
TEST_P(ArithmeticXattrOnTest, TestDocWithXattr)365 TEST_P(ArithmeticXattrOnTest, TestDocWithXattr) {
366     auto& conn = getConnection();
367     EXPECT_EQ(0, conn.increment(name, 1));
368 
369     // Add an xattr
370     {
371         BinprotSubdocCommand cmd;
372         cmd.setOp(PROTOCOL_BINARY_CMD_SUBDOC_DICT_ADD);
373         cmd.setKey(name);
374         cmd.setPath("meta.author");
375         cmd.setValue("\"Trond Norbye\"");
376         cmd.addPathFlags(SUBDOC_FLAG_XATTR_PATH | SUBDOC_FLAG_MKDIR_P);
377         conn.sendCommand(cmd);
378 
379         BinprotResponse resp;
380         conn.recvResponse(resp);
381         ASSERT_TRUE(resp.isSuccess())
382                         << memcached_status_2_text(resp.getStatus());
383     }
384 
385     // Perform the normal operation
386     EXPECT_EQ(1, conn.increment(name, 1));
387 
388     // The xattr should have been preserved!
389     {
390         BinprotSubdocCommand cmd;
391         cmd.setOp(PROTOCOL_BINARY_CMD_SUBDOC_GET);
392         cmd.setKey(name);
393         cmd.setPath("meta.author");
394         cmd.addPathFlags(SUBDOC_FLAG_XATTR_PATH);
395         conn.sendCommand(cmd);
396 
397         BinprotSubdocResponse resp;
398         conn.recvResponse(resp);
399         ASSERT_TRUE(resp.isSuccess())
400                         << memcached_status_2_text(resp.getStatus());
401         EXPECT_EQ("\"Trond Norbye\"", resp.getValue());
402     }
403 }
404 
TEST_P(ArithmeticXattrOnTest, MB25402)405 TEST_P(ArithmeticXattrOnTest, MB25402) {
406     // Increment and decrement should not update the expiry time on existing
407     // documents
408     auto& conn = getConnection();
409 
410     // Start by creating the counter without expiry time
411     conn.increment(name, 1, 0, 0, nullptr);
412     // increment the counter (which should already exists causing the expiry
413     // time to be ignored)
414     conn.increment(name, 1, 0, 3600, nullptr);
415 
416     // Verify that the expiry time is still
417     BinprotSubdocMultiLookupCommand cmd;
418     cmd.setKey(name);
419     cmd.addGet("$document", SUBDOC_FLAG_XATTR_PATH);
420     conn.sendCommand(cmd);
421 
422     BinprotSubdocMultiLookupResponse multiResp;
423     conn.recvResponse(multiResp);
424 
425     auto& results = multiResp.getResults();
426 
427     EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_SUCCESS, multiResp.getStatus());
428     EXPECT_EQ(PROTOCOL_BINARY_RESPONSE_SUCCESS, results[0].status);
429 
430     // Ensure that we found all we expected and they're of the correct type:
431     unique_cJSON_ptr meta(cJSON_Parse(results[0].value.c_str()));
432     auto* obj = cJSON_GetObjectItem(meta.get(), "exptime");
433     EXPECT_NE(nullptr, obj);
434     EXPECT_EQ(cJSON_Number, obj->type);
435     EXPECT_EQ(0, obj->valueint);
436 }
437