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
18#include "tracing.h"
19
20#include "cookie.h"
21#include "executorpool.h"
22#include "memcached.h"
23#include "task.h"
24#include "tracing_types.h"
25
26#include <daemon/protocol/mcbp/command_context.h>
27
28#include <chrono>
29#include <memory>
30#include <mutex>
31
32// TODO: MB-20640 The default config should be configurable from memcached.json
33static phosphor::TraceConfig lastConfig{
34        phosphor::TraceConfig(phosphor::BufferMode::ring, 20 * 1024 * 1024)};
35static std::mutex configMutex;
36
37ENGINE_ERROR_CODE ioctlGetTracingStatus(Cookie& cookie,
38                                        const StrToStrMap&,
39                                        std::string& value) {
40    value = PHOSPHOR_INSTANCE.isEnabled() ? "enabled" : "disabled";
41    return ENGINE_SUCCESS;
42}
43
44ENGINE_ERROR_CODE ioctlGetTracingConfig(Cookie& cookie,
45                                        const StrToStrMap&,
46                                        std::string& value) {
47    std::lock_guard<std::mutex> lh(configMutex);
48    value = *lastConfig.toString();
49    return ENGINE_SUCCESS;
50}
51
52template <typename ContainerT, typename PredicateT>
53void erase_if(ContainerT& items, const PredicateT& predicate) {
54    for (auto it = items.begin(); it != items.end();) {
55        if (predicate(*it)) {
56            it = items.erase(it);
57        } else {
58            ++it;
59        }
60    }
61}
62
63Task::Status StaleTraceDumpRemover::periodicExecute() {
64    const auto now = std::chrono::steady_clock::now();
65    std::lock_guard<std::mutex> lh(traceDumps.mutex);
66
67    using value_type = decltype(TraceDumps::dumps)::value_type;
68    erase_if(traceDumps.dumps, [now, this](const value_type& dump) {
69        // If the mutex is locked then a
70        // chunk is being generated from this dump
71        auto isLocked = dump.second->mutex.try_lock();
72        if (!isLocked) {
73            return false;
74        }
75
76        std::lock_guard<std::mutex> guard(dump.second->mutex, std::adopt_lock);
77        return (dump.second->last_touch + std::chrono::seconds(max_age)) <= now;
78    });
79    return Status::Continue; // always repeat
80}
81
82static TraceDumps traceDumps;
83static std::shared_ptr<StaleTraceDumpRemover> dump_remover;
84
85void initializeTracing() {
86    // Currently just creating the stale dump remover periodic task
87    // @todo make period and max_age configurable
88    dump_remover = std::make_shared<StaleTraceDumpRemover>(
89            traceDumps, std::chrono::seconds(60), std::chrono::seconds(300));
90    std::shared_ptr<Task> task = dump_remover;
91    {
92        std::lock_guard<std::mutex> lg(task->getMutex());
93        executorPool->schedule(task);
94    }
95
96    // and begin tracing.
97    {
98        std::lock_guard<std::mutex> lh(configMutex);
99        PHOSPHOR_INSTANCE.start(lastConfig);
100    }
101}
102
103void deinitializeTracing() {
104    dump_remover.reset();
105    phosphor::TraceLog::getInstance().stop();
106    std::lock_guard<std::mutex> guard(traceDumps.mutex);
107    traceDumps.dumps.clear();
108}
109
110ENGINE_ERROR_CODE ioctlGetTracingBeginDump(Cookie& cookie,
111                                           const StrToStrMap&,
112                                           std::string& value) {
113    std::lock_guard<phosphor::TraceLog> lh(PHOSPHOR_INSTANCE);
114    if (PHOSPHOR_INSTANCE.isEnabled()) {
115        PHOSPHOR_INSTANCE.stop(lh);
116    }
117
118    phosphor::TraceContext context = PHOSPHOR_INSTANCE.getTraceContext(lh);
119    if (context.getBuffer() == nullptr) {
120        cookie.setErrorContext(
121                "Cannot begin a dump when there is no existing trace");
122        return ENGINE_EINVAL;
123    }
124
125    // Create the new dump associated with a random uuid
126    cb::uuid::uuid_t uuid = cb::uuid::random();
127    {
128        std::lock_guard<std::mutex> lh(traceDumps.mutex);
129        traceDumps.dumps.emplace(
130                uuid, std::make_unique<DumpContext>(std::move(context)));
131    }
132
133    // Return the textual form of the uuid back to the user with success
134    value = to_string(uuid);
135    return ENGINE_SUCCESS;
136}
137
138/**
139 * A task for generating tasks in the background on an
140 * executor thread instead of a front-end thread
141 */
142class ChunkBuilderTask : public Task {
143public:
144    /// This constructor assumes that the dump's
145    /// mutex has already been locked
146    ChunkBuilderTask(Cookie& cookie_,
147                     DumpContext& dump,
148                     std::unique_lock<std::mutex> lck,
149                     size_t chunk_size)
150        : Task(), cookie(cookie_), dump(dump), lck(std::move(lck)) {
151        chunk.resize(chunk_size);
152    }
153
154    Status execute() override {
155        size_t count = dump.json_export.read(&chunk[0], chunk.size());
156        chunk.resize(count);
157        return Status::Finished;
158    }
159
160    void notifyExecutionComplete() override {
161        notify_io_complete(&cookie, ENGINE_SUCCESS);
162    }
163
164    std::string& getChunk() {
165        return chunk;
166    }
167
168private:
169    std::string chunk;
170    Cookie& cookie;
171    DumpContext& dump;
172    std::unique_lock<std::mutex> lck;
173};
174
175struct ChunkBuilderContext : public CommandContext {
176    ChunkBuilderContext(std::shared_ptr<ChunkBuilderTask>& task) : task(task) {
177    }
178
179    std::shared_ptr<ChunkBuilderTask> task;
180};
181
182ENGINE_ERROR_CODE ioctlGetTracingDumpChunk(Cookie& cookie,
183                                           const StrToStrMap& arguments,
184                                           std::string& value) {
185    // If we have a context then we already generated the chunk
186    auto* ctx = dynamic_cast<ChunkBuilderContext*>(cookie.getCommandContext());
187    if (ctx != nullptr) {
188        value = std::move(ctx->task->getChunk());
189        cookie.setCommandContext();
190        return ENGINE_SUCCESS;
191    }
192
193    auto id = arguments.find("id");
194    if (id == arguments.end()) {
195        cookie.setErrorContext("Dump ID must be specified as a key argument");
196        return ENGINE_EINVAL;
197    }
198
199    cb::uuid::uuid_t uuid;
200    try {
201        uuid = cb::uuid::from_string(id->second);
202    } catch (const std::invalid_argument&) {
203        cookie.setErrorContext("Dump ID must be a valid UUID");
204        return ENGINE_EINVAL;
205    }
206
207    {
208        std::lock_guard<std::mutex> lh(traceDumps.mutex);
209        auto iter = traceDumps.dumps.find(uuid);
210        if (iter == traceDumps.dumps.end()) {
211            cookie.setErrorContext(
212                    "Dump ID must correspond to an existing dump");
213            return ENGINE_EINVAL;
214        }
215        auto& dump = *(iter->second);
216
217        // @todo make configurable
218        const size_t chunk_size = 1024 * 1024;
219
220        if (dump.json_export.done()) {
221            value = "";
222            return ENGINE_SUCCESS;
223        }
224
225        std::unique_lock<std::mutex> lck(dump.mutex, std::try_to_lock);
226        // A chunk is already being generated for this dump
227        if (!lck) {
228            value = "";
229            cookie.setErrorContext(
230                    "A chunk is already being fetched for this dump");
231            return ENGINE_TMPFAIL;
232        }
233
234        // ChunkBuilderTask assumes the lock above is already held
235        auto task = std::make_shared<ChunkBuilderTask>(
236                cookie, dump, std::move(lck), chunk_size);
237        cookie.setCommandContext(new ChunkBuilderContext{task});
238
239        cookie.setEwouldblock(true);
240        std::lock_guard<std::mutex> guard(task->getMutex());
241        std::shared_ptr<Task> basicTask = task;
242        executorPool->schedule(basicTask, true);
243
244        return ENGINE_EWOULDBLOCK;
245    }
246}
247
248ENGINE_ERROR_CODE ioctlSetTracingClearDump(Cookie& cookie,
249                                           const StrToStrMap& arguments,
250                                           const std::string& value) {
251    cb::uuid::uuid_t uuid;
252    try {
253        uuid = cb::uuid::from_string(value);
254    } catch (const std::invalid_argument&) {
255        cookie.setErrorContext("Dump ID must be a valid UUID");
256        return ENGINE_EINVAL;
257    }
258
259    {
260        std::lock_guard<std::mutex> lh(traceDumps.mutex);
261        auto dump = traceDumps.dumps.find(uuid);
262        if (dump == traceDumps.dumps.end()) {
263            cookie.setErrorContext(
264                    "Dump ID must correspond to an existing dump");
265            return ENGINE_EINVAL;
266        }
267
268        traceDumps.dumps.erase(dump);
269    }
270
271    return ENGINE_SUCCESS;
272}
273
274ENGINE_ERROR_CODE ioctlSetTracingConfig(Cookie& cookie,
275                                        const StrToStrMap&,
276                                        const std::string& value) {
277    if (value.empty()) {
278        cookie.setErrorContext("Trace config cannot be empty");
279        return ENGINE_EINVAL;
280    }
281    try {
282        std::lock_guard<std::mutex> lh(configMutex);
283        lastConfig = phosphor::TraceConfig::fromString(value);
284    } catch (const std::invalid_argument& e) {
285        cookie.setErrorContext(std::string("Trace config is illformed: ") +
286                               e.what());
287        return ENGINE_EINVAL;
288    }
289    return ENGINE_SUCCESS;
290}
291
292ENGINE_ERROR_CODE ioctlSetTracingStart(Cookie& cookie,
293                                       const StrToStrMap&,
294                                       const std::string& value) {
295    std::lock_guard<std::mutex> lh(configMutex);
296    PHOSPHOR_INSTANCE.start(lastConfig);
297    return ENGINE_SUCCESS;
298}
299
300ENGINE_ERROR_CODE ioctlSetTracingStop(Cookie& cookie,
301                                      const StrToStrMap&,
302                                      const std::string& value) {
303    PHOSPHOR_INSTANCE.stop();
304    return ENGINE_SUCCESS;
305}
306