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
33 static phosphor::TraceConfig lastConfig{
34         phosphor::TraceConfig(phosphor::BufferMode::ring, 20 * 1024 * 1024)};
35 static std::mutex configMutex;
36 
ioctlGetTracingStatus(Cookie& cookie, const StrToStrMap&, std::string& value)37 ENGINE_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 
ioctlGetTracingConfig(Cookie& cookie, const StrToStrMap&, std::string& value)44 ENGINE_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 
52 template <typename ContainerT, typename PredicateT>
erase_if(ContainerT& items, const PredicateT& predicate)53 void 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 
periodicExecute()63 Task::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 
82 static TraceDumps traceDumps;
83 static std::shared_ptr<StaleTraceDumpRemover> dump_remover;
84 
initializeTracing()85 void 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 
deinitializeTracing()103 void 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 
ioctlGetTracingBeginDump(Cookie& cookie, const StrToStrMap&, std::string& value)110 ENGINE_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  */
142 class ChunkBuilderTask : public Task {
143 public:
144     /// This constructor assumes that the dump's
145     /// mutex has already been locked
ChunkBuilderTask(Cookie& cookie_, DumpContext& dump, std::unique_lock<std::mutex> lck, size_t chunk_size)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 
getChunk()164     std::string& getChunk() {
165         return chunk;
166     }
167 
168 private:
169     std::string chunk;
170     Cookie& cookie;
171     DumpContext& dump;
172     std::unique_lock<std::mutex> lck;
173 };
174 
175 struct ChunkBuilderContext : public CommandContext {
ChunkBuilderContextChunkBuilderContext176     ChunkBuilderContext(std::shared_ptr<ChunkBuilderTask>& task) : task(task) {
177     }
178 
179     std::shared_ptr<ChunkBuilderTask> task;
180 };
181 
ioctlGetTracingDumpChunk(Cookie& cookie, const StrToStrMap& arguments, std::string& value)182 ENGINE_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 
ioctlSetTracingClearDump(Cookie& cookie, const StrToStrMap& arguments, const std::string& value)248 ENGINE_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 
ioctlSetTracingConfig(Cookie& cookie, const StrToStrMap&, const std::string& value)274 ENGINE_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 
ioctlSetTracingStart(Cookie& cookie, const StrToStrMap&, const std::string& value)292 ENGINE_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 
ioctlSetTracingStop(Cookie& cookie, const StrToStrMap&, const std::string& value)300 ENGINE_ERROR_CODE ioctlSetTracingStop(Cookie& cookie,
301                                       const StrToStrMap&,
302                                       const std::string& value) {
303     PHOSPHOR_INSTANCE.stop();
304     return ENGINE_SUCCESS;
305 }
306