From c8758406b5685457992448fcca5b07c92c37b37b Mon Sep 17 00:00:00 2001 From: Sam McCall Date: Thu, 13 Sep 2018 11:47:48 +0000 Subject: [PATCH] [clangd] Simplify cancellation public API Summary: Task is no longer exposed: - task cancellation is hidden as a std::function - task creation returns the new context directly - checking is via free function only, with no way to avoid the context lookup The implementation is essentially the same, but a bit terser as it's hidden. isCancelled() is now safe to use outside any task (it returns false). This will leave us free to sprinkle cancellation in e.g. TUScheduler without needing elaborate test setup, and lets callers that don't cancel "just work". Updated the docs to describe the new expected use pattern. One thing I noticed: there's nothing async-specific about the cancellation. Async tasks can be cancelled from any thread (typically the one that created them), sync tasks can be cancelled from any *other* thread in the same way. So the docs now refer to "long-running" tasks instead of async ones. Updated usage in code complete, without any structural changes. I didn't update all the names of the helpers in ClangdLSPServer (these will likely be moved to JSONRPCDispatcher anyway). Reviewers: ilya-biryukov, kadircet Subscribers: ioeric, MaskRay, jkorous, arphaman, jfb, cfe-commits Differential Revision: https://reviews.llvm.org/D51996 llvm-svn: 342130 --- clang-tools-extra/clangd/Cancellation.cpp | 22 +-- clang-tools-extra/clangd/Cancellation.h | 152 ++++++++------------- clang-tools-extra/clangd/ClangdLSPServer.cpp | 9 +- clang-tools-extra/clangd/ClangdLSPServer.h | 8 +- clang-tools-extra/clangd/ClangdServer.cpp | 12 +- clang-tools-extra/clangd/ClangdServer.h | 6 +- .../unittests/clangd/CancellationTests.cpp | 41 +++--- 7 files changed, 99 insertions(+), 151 deletions(-) diff --git a/clang-tools-extra/clangd/Cancellation.cpp b/clang-tools-extra/clangd/Cancellation.cpp index a740975..ae2354b 100644 --- a/clang-tools-extra/clangd/Cancellation.cpp +++ b/clang-tools-extra/clangd/Cancellation.cpp @@ -13,21 +13,21 @@ namespace clang { namespace clangd { -namespace { -static Key TaskKey; -} // namespace - char CancelledError::ID = 0; +static Key>> FlagKey; -const Task &getCurrentTask() { - const auto TH = Context::current().getExisting(TaskKey); - assert(TH && "Fetched a nullptr for TaskHandle from context."); - return *TH; +std::pair cancelableTask() { + auto Flag = std::make_shared>(); + return { + Context::current().derive(FlagKey, Flag), + [Flag] { *Flag = true; }, + }; } -Context setCurrentTask(ConstTaskHandle TH) { - assert(TH && "Trying to stash a nullptr as TaskHandle into context."); - return Context::current().derive(TaskKey, std::move(TH)); +bool isCancelled() { + if (auto *Flag = Context::current().get(FlagKey)) + return **Flag; + return false; // Not in scope of a task. } } // namespace clangd diff --git a/clang-tools-extra/clangd/Cancellation.h b/clang-tools-extra/clangd/Cancellation.h index 58f8456..b6d6050 100644 --- a/clang-tools-extra/clangd/Cancellation.h +++ b/clang-tools-extra/clangd/Cancellation.h @@ -6,124 +6,82 @@ // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// -// Cancellation mechanism for async tasks. Roughly all the clients of this code -// can be classified into three categories: -// 1. The code that creates and schedules async tasks, e.g. TUScheduler. -// 2. The callers of the async method that can cancel some of the running tasks, -// e.g. `ClangdLSPServer` -// 3. The code running inside the async task itself, i.e. code completion or -// find definition implementation that run clang, etc. +// Cancellation mechanism for long-running tasks. // -// For (1), the guideline is to accept a callback for the result of async -// operation and return a `TaskHandle` to allow cancelling the request. +// This manages interactions between: // -// TaskHandle someAsyncMethod(Runnable T, -// function)> Callback) { -// auto TH = Task::createHandle(); -// WithContext ContextWithCancellationToken(TH); -// auto run = [](){ -// Callback(T()); +// 1. Client code that starts some long-running work, and maybe cancels later. +// +// std::pair Task = cancelableTask(); +// { +// WithContext Cancelable(std::move(Task.first)); +// Expected +// deepThoughtAsync([](int answer){ errs() << answer; }); // } -// // Start run() in a new async thread, and make sure to propagate Context. -// return TH; -// } +// // ...some time later... +// if (User.fellAsleep()) +// Task.second(); +// +// (This example has an asynchronous computation, but synchronous examples +// work similarly - the Canceler should be invoked from another thread). +// +// 2. Library code that executes long-running work, and can exit early if the +// result is not needed. // -// The callers of async methods (2) can issue cancellations and should be -// prepared to handle `TaskCancelledError` result: +// void deepThoughtAsync(std::function Callback) { +// runAsync([Callback]{ +// int A = ponder(6); +// if (isCancelled()) +// return; +// int B = ponder(9); +// if (isCancelled()) +// return; +// Callback(A * B); +// }); +// } // -// void Caller() { -// // You should store this handle if you wanna cancel the task later on. -// TaskHandle TH = someAsyncMethod(Task, [](llvm::Expected R) { -// if(/*check for task cancellation error*/) -// // Handle the error -// // Do other things on R. -// }); -// // To cancel the task: -// sleep(5); -// TH->cancel(); -// } +// (A real example may invoke the callback with an error on cancellation, +// the CancelledError is provided for this purpose). // -// The worker code itself (3) should check for cancellations using -// `Task::isCancelled` that can be retrieved via `getCurrentTask()`. +// Cancellation has some caveats: +// - the work will only stop when/if the library code next checks for it. +// Code outside clangd such as Sema will not do this. +// - it's inherently racy: client code must be prepared to accept results +// even after requesting cancellation. +// - it's Context-based, so async work must be dispatched to threads in +// ways that preserve the context. (Like runAsync() or TUScheduler). // -// llvm::Expected AsyncTask() { -// // You can either store the read only TaskHandle by calling getCurrentTask -// // once and just use the variable everytime you want to check for -// // cancellation, or call isCancelled everytime. The former is more -// // efficient if you are going to have multiple checks. -// const auto T = getCurrentTask(); -// // DO SMTHNG... -// if(T.isCancelled()) { -// // Task has been cancelled, lets get out. -// return llvm::makeError(); -// } -// // DO SOME MORE THING... -// if(T.isCancelled()) { -// // Task has been cancelled, lets get out. -// return llvm::makeError(); -// } -// return ResultType(...); -// } -// If the operation was cancelled before task could run to completion, it should -// propagate the TaskCancelledError as a result. +// FIXME: We could add timestamps to isCancelled() and CancelledError. +// Measuring the start -> cancel -> acknowledge -> finish timeline would +// help find where libraries' cancellation should be improved. #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CANCELLATION_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CANCELLATION_H #include "Context.h" #include "llvm/Support/Error.h" -#include -#include +#include #include namespace clang { namespace clangd { -/// Enables signalling a cancellation on an async task or checking for -/// cancellation. It is thread-safe to trigger cancellation from multiple -/// threads or check for cancellation. Task object for the currently running -/// task can be obtained via clangd::getCurrentTask(). -class Task { -public: - void cancel() { CT = true; } - /// If cancellation checks are rare, one could use the isCancelled() helper in - /// the namespace to simplify the code. However, if cancellation checks are - /// frequent, the guideline is first obtain the Task object for the currently - /// running task with getCurrentTask() and do cancel checks using it to avoid - /// extra lookups in the Context. - bool isCancelled() const { return CT; } - - /// Creates a task handle that can be used by an async task to check for - /// information that can change during it's runtime, like Cancellation. - static std::shared_ptr createHandle() { - return std::shared_ptr(new Task()); - } - - Task(const Task &) = delete; - Task &operator=(const Task &) = delete; - Task(Task &&) = delete; - Task &operator=(Task &&) = delete; - -private: - Task() : CT(false) {} - std::atomic CT; -}; -using ConstTaskHandle = std::shared_ptr; -using TaskHandle = std::shared_ptr; - -/// Fetches current task information from Context. TaskHandle must have been -/// stashed into context beforehand. -const Task &getCurrentTask(); +/// A canceller requests cancellation of a task, when called. +/// Calling it again has no effect. +using Canceler = std::function; -/// Stashes current task information within the context. -LLVM_NODISCARD Context setCurrentTask(ConstTaskHandle TH); +/// Defines a new task whose cancellation may be requested. +/// The returned Context defines the scope of the task. +/// When the context is active, isCancelled() is false until the Canceler is +/// invoked, and true afterwards. +std::pair cancelableTask(); -/// Checks whether the current task has been cancelled or not. -/// Consider storing the task handler returned by getCurrentTask and then -/// calling isCancelled through it. getCurrentTask is expensive since it does a -/// lookup in the context. -inline bool isCancelled() { return getCurrentTask().isCancelled(); } +/// True if the current context is within a cancelable task which was cancelled. +/// Always false if there is no active cancelable task. +/// This isn't free (context lookup) - don't call it in a tight loop. +bool isCancelled(); +/// Conventional error when no result is returned due to cancellation. class CancelledError : public llvm::ErrorInfo { public: static char ID; diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp index 53dc8ac..c01e541 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -348,7 +348,7 @@ void ClangdLSPServer::onCodeAction(CodeActionParams &Params) { void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) { CreateSpaceForTaskHandle(); - TaskHandle TH = Server.codeComplete( + Canceler Cancel = Server.codeComplete( Params.textDocument.uri.file(), Params.position, CCOpts, [this](llvm::Expected List) { auto _ = llvm::make_scope_exit([this]() { CleanupTaskHandle(); }); @@ -361,7 +361,7 @@ void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) { LSPList.items.push_back(R.render(CCOpts)); return reply(std::move(LSPList)); }); - StoreTaskHandle(std::move(TH)); + StoreTaskHandle(std::move(Cancel)); } void ClangdLSPServer::onSignatureHelp(TextDocumentPositionParams &Params) { @@ -635,8 +635,7 @@ void ClangdLSPServer::onCancelRequest(CancelParams &Params) { const auto &It = TaskHandles.find(Params.ID); if (It == TaskHandles.end()) return; - if (It->second) - It->second->cancel(); + It->second(); TaskHandles.erase(It); } @@ -659,7 +658,7 @@ void ClangdLSPServer::CreateSpaceForTaskHandle() { elog("Creation of space for task handle: {0} failed.", NormalizedID); } -void ClangdLSPServer::StoreTaskHandle(TaskHandle TH) { +void ClangdLSPServer::StoreTaskHandle(Canceler TH) { const json::Value *ID = getRequestId(); if (!ID) return; diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h index d717de5..4f444ea 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -170,9 +170,9 @@ private: // destructed instance of ClangdLSPServer. ClangdServer Server; - // Holds task handles for running requets. Key of the map is a serialized - // request id. - llvm::StringMap TaskHandles; + // Holds cancelers for running requets. Key of the map is a serialized + // request id. FIXME: handle cancellation generically in JSONRPCDispatcher. + llvm::StringMap TaskHandles; std::mutex TaskHandlesMutex; // Following three functions are for managing TaskHandles map. They store or @@ -183,7 +183,7 @@ private: // request. void CleanupTaskHandle(); void CreateSpaceForTaskHandle(); - void StoreTaskHandle(TaskHandle TH); + void StoreTaskHandle(Canceler TH); }; } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp index 0a8e748..20af72f 100644 --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -201,16 +201,16 @@ void ClangdServer::removeDocument(PathRef File) { WorkScheduler.remove(File); } -TaskHandle ClangdServer::codeComplete(PathRef File, Position Pos, - const clangd::CodeCompleteOptions &Opts, - Callback CB) { +Canceler ClangdServer::codeComplete(PathRef File, Position Pos, + const clangd::CodeCompleteOptions &Opts, + Callback CB) { // Copy completion options for passing them to async task handler. auto CodeCompleteOpts = Opts; if (!CodeCompleteOpts.Index) // Respect overridden index. CodeCompleteOpts.Index = Index; - TaskHandle TH = Task::createHandle(); - WithContext ContextWithCancellation(setCurrentTask(TH)); + auto Cancelable = cancelableTask(); + WithContext ContextWithCancellation(std::move(Cancelable.first)); // Copy PCHs to avoid accessing this->PCHs concurrently std::shared_ptr PCHs = this->PCHs; auto FS = FSProvider.getFileSystem(); @@ -259,7 +259,7 @@ TaskHandle ClangdServer::codeComplete(PathRef File, Position Pos, // We use a potentially-stale preamble because latency is critical here. WorkScheduler.runWithPreamble("CodeComplete", File, TUScheduler::Stale, Bind(Task, File.str(), std::move(CB))); - return TH; + return std::move(Cancelable.second); } void ClangdServer::signatureHelp(PathRef File, Position Pos, diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h index e459fcc..41b99b5 100644 --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -125,9 +125,9 @@ public: /// while returned future is not yet ready. /// A version of `codeComplete` that runs \p Callback on the processing thread /// when codeComplete results become available. - TaskHandle codeComplete(PathRef File, Position Pos, - const clangd::CodeCompleteOptions &Opts, - Callback CB); + Canceler codeComplete(PathRef File, Position Pos, + const clangd::CodeCompleteOptions &Opts, + Callback CB); /// Provide signature help for \p File at \p Pos. This method should only be /// called for tracked files. diff --git a/clang-tools-extra/unittests/clangd/CancellationTests.cpp b/clang-tools-extra/unittests/clangd/CancellationTests.cpp index 7afd108..611ce07 100644 --- a/clang-tools-extra/unittests/clangd/CancellationTests.cpp +++ b/clang-tools-extra/unittests/clangd/CancellationTests.cpp @@ -13,57 +13,48 @@ namespace clangd { namespace { TEST(CancellationTest, CancellationTest) { - TaskHandle TH = Task::createHandle(); - WithContext ContextWithCancellation(setCurrentTask(TH)); + auto Task = cancelableTask(); + WithContext ContextWithCancellation(std::move(Task.first)); EXPECT_FALSE(isCancelled()); - TH->cancel(); + Task.second(); EXPECT_TRUE(isCancelled()); } -TEST(CancellationTest, TaskTestHandleDiesContextLives) { +TEST(CancellationTest, CancelerDiesContextLives) { llvm::Optional ContextWithCancellation; { - TaskHandle TH = Task::createHandle(); - ContextWithCancellation.emplace(setCurrentTask(TH)); + auto Task = cancelableTask(); + ContextWithCancellation.emplace(std::move(Task.first)); EXPECT_FALSE(isCancelled()); - TH->cancel(); + Task.second(); EXPECT_TRUE(isCancelled()); } EXPECT_TRUE(isCancelled()); } TEST(CancellationTest, TaskContextDiesHandleLives) { - TaskHandle TH = Task::createHandle(); + auto Task = cancelableTask(); { - WithContext ContextWithCancellation(setCurrentTask(TH)); + WithContext ContextWithCancellation(std::move(Task.first)); EXPECT_FALSE(isCancelled()); - TH->cancel(); + Task.second(); EXPECT_TRUE(isCancelled()); } // Still should be able to cancel without any problems. - TH->cancel(); -} - -TEST(CancellationTest, CancellationToken) { - TaskHandle TH = Task::createHandle(); - WithContext ContextWithCancellation(setCurrentTask(TH)); - const auto &CT = getCurrentTask(); - EXPECT_FALSE(CT.isCancelled()); - TH->cancel(); - EXPECT_TRUE(CT.isCancelled()); + Task.second(); } TEST(CancellationTest, AsynCancellationTest) { std::atomic HasCancelled(false); Notification Cancelled; - auto TaskToBeCancelled = [&](ConstTaskHandle CT) { - WithContext ContextGuard(setCurrentTask(std::move(CT))); + auto TaskToBeCancelled = [&](Context Ctx) { + WithContext ContextGuard(std::move(Ctx)); Cancelled.wait(); HasCancelled = isCancelled(); }; - TaskHandle TH = Task::createHandle(); - std::thread AsyncTask(TaskToBeCancelled, TH); - TH->cancel(); + auto Task = cancelableTask(); + std::thread AsyncTask(TaskToBeCancelled, std::move(Task.first)); + Task.second(); Cancelled.notify(); AsyncTask.join(); -- 2.7.4