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
22class StatsTest : public TestappClientTest {
23public:
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
31protected:
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
41INSTANTIATE_TEST_CASE_P(TransportProtocols,
42                        StatsTest,
43                        ::testing::Values(TransportProtocols::McbpPlain,
44                                          TransportProtocols::McbpSsl),
45                        ::testing::PrintToStringParamName());
46
47TEST_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
56TEST_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
99TEST_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
113TEST_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 */
156TEST_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 */
195TEST_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 */
239TEST_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
263TEST_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
284TEST_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
295TEST_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
307TEST_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
319TEST_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
349TEST_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
355TEST_P(StatsTest, TestSchedulerInfo_Aggregate) {
356    auto stats = getConnection().stats("worker_thread_info aggregate");
357    EXPECT_NE(stats.end(), stats.find("aggregate"));
358}
359
360TEST_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
369TEST_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
377TEST_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
414TEST_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
424TEST_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
435TEST_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
452TEST_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
479TEST_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
488TEST_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
497TEST_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
511TEST_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
524TEST_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 */
562class NoBucketStatsTest : public StatsTest {
563public:
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
584TEST_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
592INSTANTIATE_TEST_CASE_P(TransportProtocols,
593                        NoBucketStatsTest,
594                        ::testing::Values(TransportProtocols::McbpPlain,
595                                          TransportProtocols::McbpSsl),
596                        ::testing::PrintToStringParamName());
597