1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3  *     Copyright 2015 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 #include "testapp_client_test.h"
19 #include <protocol/mcbp/ewb_encode.h>
20 #include <gsl/gsl>
21 
22 class StatsTest : public TestappClientTest {
23 public:
SetUp()24     void SetUp() {
25         TestappClientTest::SetUp();
26         // Let all tests start with an empty set of stats (There is
27         // a special test case that tests that reset actually work)
28         resetBucket();
29     }
30 
31 protected:
resetBucket()32     void resetBucket() {
33         MemcachedConnection& conn = getConnection();
34         ASSERT_NO_THROW(conn.authenticate("@admin", "password", "PLAIN"));
35         ASSERT_NO_THROW(conn.selectBucket("default"));
36         ASSERT_NO_THROW(conn.stats("reset"));
37         ASSERT_NO_THROW(conn.reconnect());
38     }
39 };
40 
41 INSTANTIATE_TEST_CASE_P(TransportProtocols,
42                         StatsTest,
43                         ::testing::Values(TransportProtocols::McbpPlain,
44                                           TransportProtocols::McbpSsl),
45                         ::testing::PrintToStringParamName());
46 
TEST_P(StatsTest, TestDefaultStats)47 TEST_P(StatsTest, TestDefaultStats) {
48     MemcachedConnection& conn = getConnection();
49     auto stats = conn.stats("");
50 
51     // Don't expect the entire stats set, but we should at least have
52     // the uptime
53     EXPECT_NE(stats.end(), stats.find("uptime"));
54 }
55 
TEST_P(StatsTest, TestGetMeta)56 TEST_P(StatsTest, TestGetMeta) {
57     MemcachedConnection& conn = getConnection();
58 
59     // Set a document
60     Document doc;
61     doc.info.cas = mcbp::cas::Wildcard;
62     doc.info.flags = 0xcaffee;
63     doc.info.id = name;
64     doc.value = memcached_cfg.dump();
65     conn.mutate(doc, Vbid(0), MutationType::Set);
66 
67     // Send 10 GET_META, this should not increase the `cmd_get` and `get_hits` stats
68     for (int i = 0; i < 10; i++) {
69         auto meta = conn.getMeta(doc.info.id, Vbid(0), GetMetaVersion::V1);
70         EXPECT_EQ(cb::mcbp::Status::Success, meta.first);
71     }
72     auto stats = conn.stats("");
73 
74     auto cmd_get = stats["cmd_get"].get<size_t>();
75     EXPECT_EQ(0, cmd_get);
76 
77     auto get_hits = stats["get_hits"].get<size_t>();
78     EXPECT_EQ(0, get_hits);
79 
80     // Now, send 10 GET_META for a document that does not exist, this should
81     // not increase the `cmd_get` and `get_misses` stats or the `get_hits`
82     // stat
83     for (int i = 0; i < 10; i++) {
84         auto meta = conn.getMeta("no_key", Vbid(0), GetMetaVersion::V1);
85         EXPECT_EQ(cb::mcbp::Status::KeyEnoent, meta.first);
86     }
87     stats = conn.stats("");
88 
89     cmd_get = stats["cmd_get"].get<size_t>();
90     EXPECT_EQ(0, cmd_get);
91 
92     auto get_misses = stats["get_misses"].get<size_t>();
93     EXPECT_EQ(0, get_misses);
94 
95     get_hits = stats["get_hits"].get<size_t>();
96     EXPECT_EQ(0, get_hits);
97 }
98 
TEST_P(StatsTest, StatsResetIsPrivileged)99 TEST_P(StatsTest, StatsResetIsPrivileged) {
100     MemcachedConnection& conn = getConnection();
101 
102     try {
103         conn.stats("reset");
104         FAIL() << "reset is a privileged operation";
105     } catch (ConnectionError& error) {
106         EXPECT_TRUE(error.isAccessDenied());
107     }
108 
109     conn.authenticate("@admin", "password", "PLAIN");
110     conn.stats("reset");
111 }
112 
TEST_P(StatsTest, TestReset)113 TEST_P(StatsTest, TestReset) {
114     MemcachedConnection& conn = getConnection();
115 
116     auto stats = conn.stats("");
117     ASSERT_FALSE(stats.empty());
118 
119     auto before = stats["cmd_get"].get<size_t>();
120 
121     for (int ii = 0; ii < 10; ++ii) {
122         EXPECT_THROW(conn.get("foo", Vbid(0)), ConnectionError);
123     }
124 
125     stats = conn.stats("");
126     EXPECT_NE(before, stats["cmd_get"].get<size_t>());
127 
128     // the cmd_get counter does work.. now check that reset sets it back..
129     resetBucket();
130 
131     stats = conn.stats("");
132     EXPECT_EQ(0, stats["cmd_get"].get<size_t>());
133 
134     // Just ensure that the "reset timings" is detected
135     // @todo add a separate test case for cmd timings stats
136     conn.authenticate("@admin", "password", "PLAIN");
137     conn.selectBucket("default");
138     stats = conn.stats("reset timings");
139 
140     // Just ensure that the "reset bogus" is detected..
141     try {
142         conn.stats("reset bogus");
143         FAIL()<<"stats reset bogus should throw an exception (non a valid cmd)";
144     } catch (ConnectionError& error) {
145         EXPECT_TRUE(error.isInvalidArguments());
146     }
147     conn.reconnect();
148 }
149 
150 /**
151  * MB-17815: The cmd_set stat is incremented multiple times if the underlying
152  * engine returns EWOULDBLOCK (which would happen for all operations when
153  * the underlying engine is operating in full eviction mode and the document
154  * isn't resident)
155  */
TEST_P(StatsTest, Test_MB_17815)156 TEST_P(StatsTest, Test_MB_17815) {
157     MemcachedConnection& conn = getConnection();
158 
159     auto stats = conn.stats("");
160     EXPECT_EQ(0, stats["cmd_set"].get<size_t>());
161 
162     auto sequence = ewb::encodeSequence({cb::engine_errc::would_block,
163                                          cb::engine_errc::success,
164                                          ewb::Passthrough,
165                                          cb::engine_errc::would_block,
166                                          cb::engine_errc::success,
167                                          ewb::Passthrough});
168     conn.configureEwouldBlockEngine(EWBEngineMode::Sequence,
169                                     /*unused*/ {},
170                                     /*unused*/ {},
171                                     sequence);
172 
173     Document doc;
174     doc.info.cas = mcbp::cas::Wildcard;
175     doc.info.flags = 0xcaffee;
176     doc.info.id = name;
177     doc.value = memcached_cfg.dump();
178 
179     conn.mutate(doc, Vbid(0), MutationType::Add);
180 
181     conn.disableEwouldBlockEngine();
182 
183     stats = conn.stats("");
184     EXPECT_EQ(1, stats["cmd_set"].get<size_t>());
185 }
186 
187 /**
188  * MB-17815: The cmd_set stat is incremented multiple times if the underlying
189  * engine returns EWOULDBLOCK (which would happen for all operations when
190  * the underlying engine is operating in full eviction mode and the document
191  * isn't resident). This test is specfically testing this error case with
192  * append (due to MB-28850) rather than the other MB-17815 test which tests
193  * Add.
194  */
TEST_P(StatsTest, Test_MB_17815_Append)195 TEST_P(StatsTest, Test_MB_17815_Append) {
196     MemcachedConnection& conn = getConnection();
197 
198     auto stats = conn.stats("");
199     EXPECT_EQ(0, stats["cmd_set"].get<size_t>());
200 
201     // Allow first SET to succeed and then return EWOULDBLOCK for
202     // the Append (2nd op).
203 
204     // Set a document
205     Document doc;
206     doc.info.cas = mcbp::cas::Wildcard;
207     doc.info.flags = 0xcaffee;
208     doc.info.id = name;
209     doc.value = memcached_cfg.dump();
210     conn.mutate(doc, Vbid(0), MutationType::Set);
211 
212     // bucket_get -> Passthrough,
213     // bucket_allocate -> Passthrough,
214     // bucket_CAS -> EWOULDBLOCK (success)
215     // bucket_CAS (retry) -> Passthrough
216     auto sequence = ewb::encodeSequence({ewb::Passthrough,
217                                          ewb::Passthrough,
218                                          cb::engine_errc::would_block,
219                                          cb::engine_errc::success,
220                                          ewb::Passthrough});
221     conn.configureEwouldBlockEngine(EWBEngineMode::Sequence,
222                                     /*unused*/ {},
223                                     /*unused*/ {},
224                                     sequence);
225 
226     // Now append to the same doc
227     conn.mutate(doc, Vbid(0), MutationType::Append);
228 
229     conn.disableEwouldBlockEngine();
230 
231     stats = conn.stats("");
232     EXPECT_EQ(2, stats["cmd_set"].get<size_t>());
233 }
234 
235 /**
236  * Verify that cmd_set is updated when we fail to perform the
237  * append operation.
238  */
TEST_P(StatsTest, Test_MB_29259_Append)239 TEST_P(StatsTest, Test_MB_29259_Append) {
240     MemcachedConnection& conn = getConnection();
241 
242     auto stats = conn.stats("");
243     EXPECT_EQ(0, stats["cmd_set"].get<size_t>());
244 
245     Document doc;
246     doc.info.cas = mcbp::cas::Wildcard;
247     doc.info.flags = 0xcaffee;
248     doc.info.id = name;
249     doc.value = memcached_cfg.dump();
250 
251     // Try to append to non-existing document
252     try {
253         conn.mutate(doc, Vbid(0), MutationType::Append);
254         FAIL() << "Append on non-existing document should fail";
255     } catch (const ConnectionError& error) {
256         EXPECT_TRUE(error.isNotStored());
257     }
258 
259     stats = conn.stats("");
260     EXPECT_EQ(1, stats["cmd_set"].get<size_t>());
261 }
262 
TEST_P(StatsTest, TestAppend)263 TEST_P(StatsTest, TestAppend) {
264     MemcachedConnection& conn = getConnection();
265 
266     // Set a document
267     Document doc;
268     doc.info.cas = mcbp::cas::Wildcard;
269     doc.info.flags = 0xcaffee;
270     doc.info.id = name;
271     doc.value = memcached_cfg.dump();
272     conn.mutate(doc, Vbid(0), MutationType::Set);
273 
274     // Send 10 appends, this should increase the `cmd_set` stat by 10
275     for (int i = 0; i < 10; i++) {
276         conn.mutate(doc, Vbid(0), MutationType::Append);
277     }
278     auto stats = conn.stats("");
279     // In total we expect 11 sets, since there was the initial set
280     // and then 10 appends
281     EXPECT_EQ(11, stats["cmd_set"].get<size_t>());
282 }
283 
TEST_P(StatsTest, TestAuditNoAccess)284 TEST_P(StatsTest, TestAuditNoAccess) {
285     MemcachedConnection& conn = getConnection();
286 
287     try {
288         conn.stats("audit");
289         FAIL() << "stats audit should throw an exception (non privileged)";
290     } catch (ConnectionError& error) {
291         EXPECT_TRUE(error.isAccessDenied());
292     }
293 }
294 
TEST_P(StatsTest, TestAudit)295 TEST_P(StatsTest, TestAudit) {
296     MemcachedConnection& conn = getConnection();
297     conn.authenticate("@admin", "password", "PLAIN");
298 
299     auto stats = conn.stats("audit");
300     EXPECT_EQ(2, stats.size());
301     EXPECT_EQ(false, stats["enabled"].get<bool>());
302     EXPECT_EQ(0, stats["dropped_events"].get<size_t>());
303 
304     conn.reconnect();
305 }
306 
TEST_P(StatsTest, TestBucketDetailsNoAccess)307 TEST_P(StatsTest, TestBucketDetailsNoAccess) {
308     MemcachedConnection& conn = getConnection();
309 
310     try {
311         conn.stats("bucket_details");
312         FAIL() <<
313                "stats bucket_details should throw an exception (non privileged)";
314     } catch (ConnectionError& error) {
315         EXPECT_TRUE(error.isAccessDenied());
316     }
317 }
318 
TEST_P(StatsTest, TestBucketDetails)319 TEST_P(StatsTest, TestBucketDetails) {
320     MemcachedConnection& conn = getConnection();
321     conn.authenticate("@admin", "password", "PLAIN");
322 
323     auto stats = conn.stats("bucket_details");
324     ASSERT_EQ(1, stats.size());
325     ASSERT_EQ("bucket details", stats.begin().key());
326 
327     // bucket details contains a single entry which is named "buckets" and
328     // contains an array
329     auto array = stats.front()["buckets"];
330     EXPECT_EQ(nlohmann::json::value_t::array, array.type());
331 
332     // we have two bucket2, nobucket and default
333     EXPECT_EQ(2, array.size());
334 
335     // Validate each bucket entry (I should probably extend it with checking
336     // of the actual values
337     for (const auto& bucket : array) {
338         EXPECT_EQ(5, bucket.size());
339         EXPECT_NE(bucket.end(), bucket.find("index"));
340         EXPECT_NE(bucket.end(), bucket.find("state"));
341         EXPECT_NE(bucket.end(), bucket.find("clients"));
342         EXPECT_NE(bucket.end(), bucket.find("name"));
343         EXPECT_NE(bucket.end(), bucket.find("type"));
344     }
345 
346     conn.reconnect();
347 }
348 
TEST_P(StatsTest, TestSchedulerInfo)349 TEST_P(StatsTest, TestSchedulerInfo) {
350     auto stats = getConnection().stats("worker_thread_info");
351     // We should at least have an entry for the first thread
352     EXPECT_NE(stats.end(), stats.find("0"));
353 }
354 
TEST_P(StatsTest, TestSchedulerInfo_Aggregate)355 TEST_P(StatsTest, TestSchedulerInfo_Aggregate) {
356     auto stats = getConnection().stats("worker_thread_info aggregate");
357     EXPECT_NE(stats.end(), stats.find("aggregate"));
358 }
359 
TEST_P(StatsTest, TestSchedulerInfo_InvalidSubcommand)360 TEST_P(StatsTest, TestSchedulerInfo_InvalidSubcommand) {
361     try {
362         getConnection().stats("worker_thread_info foo");
363         FAIL() << "Invalid subcommand";
364     } catch (const ConnectionError& error) {
365         EXPECT_TRUE(error.isInvalidArguments());
366     }
367 }
368 
TEST_P(StatsTest, TestAggregate)369 TEST_P(StatsTest, TestAggregate) {
370     MemcachedConnection& conn = getConnection();
371     auto stats = conn.stats("aggregate");
372     // Don't expect the entire stats set, but we should at least have
373     // the uptime
374     EXPECT_NE(stats.end(), stats.find("uptime"));
375 }
376 
TEST_P(StatsTest, TestConnections)377 TEST_P(StatsTest, TestConnections) {
378     MemcachedConnection& conn = getAdminConnection();
379     conn.hello("TestConnections", "1.0", "test connections test");
380 
381     auto stats = conn.stats("connections");
382     // We have at _least_ 2 connections
383     ASSERT_LE(2, stats.size());
384 
385     int sock = -1;
386 
387     // Unfortuately they're all mapped as a " " : "json" pairs, so lets
388     // validate that at least thats true:
389     for (const auto& entry : stats) {
390         EXPECT_NE(entry.end(), entry.find("connection"));
391         if (sock == -1) {
392             auto agent = entry.find("agent_name");
393             if (agent != entry.end()) {
394                 ASSERT_EQ(nlohmann::json::value_t::string, agent->type());
395                 if (agent->get<std::string>() == "TestConnections 1.0") {
396                     auto socket = entry.find("socket");
397                     if (socket != entry.end()) {
398                         EXPECT_EQ(nlohmann::json::value_t::number_unsigned,
399                                   socket->type());
400                         sock = gsl::narrow<int>(socket->get<size_t>());
401                     }
402                 }
403             }
404         }
405     }
406 
407     ASSERT_NE(-1, sock) << "Failed to locate the connection object";
408     stats = conn.stats("connections " + std::to_string(sock));
409 
410     ASSERT_EQ(1, stats.size());
411     EXPECT_EQ(sock, stats.front()["socket"].get<size_t>());
412 }
413 
TEST_P(StatsTest, TestConnections_MB37995)414 TEST_P(StatsTest, TestConnections_MB37995) {
415     MemcachedConnection& conn = getConnection();
416     try {
417         auto stats = conn.stats("connections");
418         FAIL() << "MB-37995: stats connection require extra privileges";
419     } catch (const ConnectionError& err) {
420         ASSERT_TRUE(err.isAccessDenied());
421     }
422 }
423 
TEST_P(StatsTest, TestConnectionsInvalidNumber)424 TEST_P(StatsTest, TestConnectionsInvalidNumber) {
425     MemcachedConnection& conn = getAdminConnection();
426     try {
427         auto stats = conn.stats("connections xxx");
428         FAIL() << "Did not detect incorrect connection number";
429 
430     } catch (ConnectionError& error) {
431         EXPECT_TRUE(error.isInvalidArguments());
432     }
433 }
434 
TEST_P(StatsTest, TestTopkeys)435 TEST_P(StatsTest, TestTopkeys) {
436     MemcachedConnection& conn = getConnection();
437 
438     for (int ii = 0; ii < 10; ++ii) {
439         Document doc;
440         doc.info.cas = mcbp::cas::Wildcard;
441         doc.info.flags = 0xcaffee;
442         doc.info.id = name;
443         doc.value = memcached_cfg.dump();
444 
445         conn.mutate(doc, Vbid(0), MutationType::Set);
446     }
447 
448     auto stats = conn.stats("topkeys");
449     EXPECT_NE(stats.end(), stats.find(name));
450 }
451 
TEST_P(StatsTest, TestTopkeysJson)452 TEST_P(StatsTest, TestTopkeysJson) {
453     MemcachedConnection& conn = getConnection();
454 
455     for (int ii = 0; ii < 10; ++ii) {
456         Document doc;
457         doc.info.cas = mcbp::cas::Wildcard;
458         doc.info.flags = 0xcaffee;
459         doc.info.id = name;
460         doc.value = memcached_cfg.dump();
461 
462         conn.mutate(doc, Vbid(0), MutationType::Set);
463     }
464 
465     auto stats = conn.stats("topkeys_json").front();
466     bool found = false;
467     for (const auto& i : stats) {
468         for (const auto j : i) {
469             if (name == j["key"]) {
470                 found = true;
471                 break;
472             }
473         }
474     }
475 
476     EXPECT_TRUE(found);
477 }
478 
TEST_P(StatsTest, TestSubdocExecute)479 TEST_P(StatsTest, TestSubdocExecute) {
480     MemcachedConnection& conn = getConnection();
481     auto stats = conn.stats("subdoc_execute");
482 
483     // json returned should have zero samples as no ops have been performed
484     EXPECT_TRUE(stats.is_object());
485     EXPECT_EQ(0, stats["0"]["total"].get<uint64_t>());
486 }
487 
TEST_P(StatsTest, TestResponseStats)488 TEST_P(StatsTest, TestResponseStats) {
489     int successCount = getResponseCount(cb::mcbp::Status::Success);
490     // 2 successes expected:
491     // 1. The previous stats call sending the JSON
492     // 2. The previous stats call sending a null packet to mark end of stats
493     EXPECT_EQ(successCount + statResps(),
494               getResponseCount(cb::mcbp::Status::Success));
495 }
496 
TEST_P(StatsTest, TracingStatsIsPrivileged)497 TEST_P(StatsTest, TracingStatsIsPrivileged) {
498     MemcachedConnection& conn = getConnection();
499 
500     try {
501         conn.stats("tracing");
502         FAIL() << "tracing is a privileged operation";
503     } catch (ConnectionError& error) {
504         EXPECT_TRUE(error.isAccessDenied());
505     }
506 
507     conn.authenticate("@admin", "password", "PLAIN");
508     conn.stats("tracing");
509 }
510 
TEST_P(StatsTest, TestTracingStats)511 TEST_P(StatsTest, TestTracingStats) {
512     MemcachedConnection& conn = getConnection();
513     conn.authenticate("@admin", "password", "PLAIN");
514 
515     auto stats = conn.stats("tracing");
516 
517     // Just check that we got some stats, no need to check all of them
518     // as we don't want memcached to be testing phosphor's logic
519     EXPECT_FALSE(stats.empty());
520     auto enabled = stats.find("log_is_enabled");
521     EXPECT_NE(stats.end(), enabled);
522 }
523 
TEST_P(StatsTest, TestSingleBucketOpStats)524 TEST_P(StatsTest, TestSingleBucketOpStats) {
525     MemcachedConnection& conn = getConnection();
526     conn.authenticate("@admin", "password", "PLAIN");
527 
528     conn.selectBucket("default");
529 
530     std::string key = "key";
531 
532     // Set a document
533     Document doc;
534     doc.info.cas = mcbp::cas::Wildcard;
535     doc.info.flags = 0xcaffee;
536     doc.info.id = key;
537     doc.value = "asdf";
538 
539     // mutate to bump stat
540     conn.mutate(doc, Vbid(0), MutationType::Set);
541     // lookup to bump stat
542     conn.get(key, Vbid(0), {} /* getFrameInfo */);
543 
544     auto stats = conn.stats("");
545 
546     EXPECT_FALSE(stats.empty());
547 
548     auto lookup = stats.find("cmd_lookup");
549     auto mutation = stats.find("cmd_mutation");
550 
551     ASSERT_NE(stats.end(), lookup);
552     ASSERT_NE(stats.end(), mutation);
553 
554     EXPECT_EQ(1, int(*lookup));
555     EXPECT_EQ(1, int(*mutation));
556 }
557 
558 /**
559  * Subclass of StatsTest which doesn't have a default bucket; hence connections
560  * will intially not be associated with any bucket.
561  */
562 class NoBucketStatsTest : public StatsTest {
563 public:
SetUpTestCase()564     static void SetUpTestCase() {
565         StatsTest::SetUpTestCase();
566     }
567 
568     // Setup as usual, but delete the default bucket before starting the
569     // testcase and reconnect) so the user isn't associated with any bucket.
570     void SetUp() override {
571         StatsTest::SetUp();
572         DeleteTestBucket();
573         getConnection().reconnect();
574     }
575 
576     // Reverse of above - re-create the default bucket to keep the parent
577     // classes happy.
578     void TearDown() override {
579         CreateTestBucket();
580         StatsTest::TearDown();
581     }
582 };
583 
TEST_P(NoBucketStatsTest, TestTopkeysNoBucket)584 TEST_P(NoBucketStatsTest, TestTopkeysNoBucket) {
585     MemcachedConnection& conn = getConnection();
586     conn.authenticate("@admin", "password", "PLAIN");
587 
588     // The actual request is expected fail with a nobucket exception.
589     EXPECT_THROW(conn.stats("topkeys"), std::runtime_error);
590 }
591 
592 INSTANTIATE_TEST_CASE_P(TransportProtocols,
593                         NoBucketStatsTest,
594                         ::testing::Values(TransportProtocols::McbpPlain,
595                                           TransportProtocols::McbpSsl),
596                         ::testing::PrintToStringParamName());
597