[clang-tools-extra] r342130 - [clangd] Simplify cancellation public API

Sam McCall via cfe-commits cfe-commits at lists.llvm.org
Thu Sep 13 04:47:48 PDT 2018


Author: sammccall
Date: Thu Sep 13 04:47:48 2018
New Revision: 342130

URL: http://llvm.org/viewvc/llvm-project?rev=342130&view=rev
Log:
[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

Modified:
    clang-tools-extra/trunk/clangd/Cancellation.cpp
    clang-tools-extra/trunk/clangd/Cancellation.h
    clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp
    clang-tools-extra/trunk/clangd/ClangdLSPServer.h
    clang-tools-extra/trunk/clangd/ClangdServer.cpp
    clang-tools-extra/trunk/clangd/ClangdServer.h
    clang-tools-extra/trunk/unittests/clangd/CancellationTests.cpp

Modified: clang-tools-extra/trunk/clangd/Cancellation.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/Cancellation.cpp?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/Cancellation.cpp (original)
+++ clang-tools-extra/trunk/clangd/Cancellation.cpp Thu Sep 13 04:47:48 2018
@@ -13,21 +13,21 @@
 namespace clang {
 namespace clangd {
 
-namespace {
-static Key<ConstTaskHandle> TaskKey;
-} // namespace
-
 char CancelledError::ID = 0;
+static Key<std::shared_ptr<std::atomic<bool>>> FlagKey;
 
-const Task &getCurrentTask() {
-  const auto TH = Context::current().getExisting(TaskKey);
-  assert(TH && "Fetched a nullptr for TaskHandle from context.");
-  return *TH;
+std::pair<Context, Canceler> cancelableTask() {
+  auto Flag = std::make_shared<std::atomic<bool>>();
+  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

Modified: clang-tools-extra/trunk/clangd/Cancellation.h
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/Cancellation.h?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/Cancellation.h (original)
+++ clang-tools-extra/trunk/clangd/Cancellation.h Thu Sep 13 04:47:48 2018
@@ -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.
-//
-// For (1), the guideline is to accept a callback for the result of async
-// operation and return a `TaskHandle` to allow cancelling the request.
-//
-//  TaskHandle someAsyncMethod(Runnable T,
-//  function<void(llvm::Expected<ResultType>)> Callback) {
-//   auto TH = Task::createHandle();
-//   WithContext ContextWithCancellationToken(TH);
-//   auto run = [](){
-//     Callback(T());
+// Cancellation mechanism for long-running tasks.
+//
+// This manages interactions between:
+//
+// 1. Client code that starts some long-running work, and maybe cancels later.
+//
+//   std::pair<Context, Canceler> Task = cancelableTask();
+//   {
+//     WithContext Cancelable(std::move(Task.first));
+//     Expected
+//     deepThoughtAsync([](int answer){ errs() << answer; });
+//   }
+//   // ...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.
+//
+//   void deepThoughtAsync(std::function<void(int)> Callback) {
+//     runAsync([Callback]{
+//       int A = ponder(6);
+//       if (isCancelled())
+//         return;
+//       int B = ponder(9);
+//       if (isCancelled())
+//         return;
+//       Callback(A * B);
+//     });
 //   }
-//   // Start run() in a new async thread, and make sure to propagate Context.
-//   return TH;
-// }
-//
-// The callers of async methods (2) can issue cancellations and should be
-// prepared to handle `TaskCancelledError` result:
-//
-// void Caller() {
-//   // You should store this handle if you wanna cancel the task later on.
-//   TaskHandle TH = someAsyncMethod(Task, [](llvm::Expected<ResultType> R) {
-//     if(/*check for task cancellation error*/)
-//       // Handle the error
-//     // Do other things on R.
-//   });
-//   // To cancel the task:
-//   sleep(5);
-//   TH->cancel();
-// }
-//
-// The worker code itself (3) should check for cancellations using
-// `Task::isCancelled` that can be retrieved via `getCurrentTask()`.
-//
-// llvm::Expected<ResultType> 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<CancelledError>();
-//    }
-//    // DO SOME MORE THING...
-//    if(T.isCancelled()) {
-//      // Task has been cancelled, lets get out.
-//      return llvm::makeError<CancelledError>();
-//    }
-//    return ResultType(...);
-// }
-// If the operation was cancelled before task could run to completion, it should
-// propagate the TaskCancelledError as a result.
+//
+//   (A real example may invoke the callback with an error on cancellation,
+//   the CancelledError is provided for this purpose).
+//
+// 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).
+//
+// 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 <atomic>
-#include <memory>
+#include <functional>
 #include <system_error>
 
 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<Task> createHandle() {
-    return std::shared_ptr<Task>(new Task());
-  }
-
-  Task(const Task &) = delete;
-  Task &operator=(const Task &) = delete;
-  Task(Task &&) = delete;
-  Task &operator=(Task &&) = delete;
-
-private:
-  Task() : CT(false) {}
-  std::atomic<bool> CT;
-};
-using ConstTaskHandle = std::shared_ptr<const Task>;
-using TaskHandle = std::shared_ptr<Task>;
-
-/// Fetches current task information from Context. TaskHandle must have been
-/// stashed into context beforehand.
-const Task &getCurrentTask();
-
-/// Stashes current task information within the context.
-LLVM_NODISCARD Context setCurrentTask(ConstTaskHandle TH);
-
-/// 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(); }
+/// A canceller requests cancellation of a task, when called.
+/// Calling it again has no effect.
+using Canceler = std::function<void()>;
+
+/// 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<Context, Canceler> cancelableTask();
+
+/// 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<CancelledError> {
 public:
   static char ID;

Modified: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp (original)
+++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp Thu Sep 13 04:47:48 2018
@@ -348,7 +348,7 @@ void ClangdLSPServer::onCodeAction(CodeA
 
 void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) {
   CreateSpaceForTaskHandle();
-  TaskHandle TH = Server.codeComplete(
+  Canceler Cancel = Server.codeComplete(
       Params.textDocument.uri.file(), Params.position, CCOpts,
       [this](llvm::Expected<CodeCompleteResult> List) {
         auto _ = llvm::make_scope_exit([this]() { CleanupTaskHandle(); });
@@ -361,7 +361,7 @@ void ClangdLSPServer::onCompletion(TextD
           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(Ca
   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::CreateSpaceForTask
     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;

Modified: clang-tools-extra/trunk/clangd/ClangdLSPServer.h
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdLSPServer.h?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdLSPServer.h (original)
+++ clang-tools-extra/trunk/clangd/ClangdLSPServer.h Thu Sep 13 04:47:48 2018
@@ -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<TaskHandle> TaskHandles;
+  // Holds cancelers for running requets. Key of the map is a serialized
+  // request id. FIXME: handle cancellation generically in JSONRPCDispatcher.
+  llvm::StringMap<Canceler> 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

Modified: clang-tools-extra/trunk/clangd/ClangdServer.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdServer.cpp?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdServer.cpp (original)
+++ clang-tools-extra/trunk/clangd/ClangdServer.cpp Thu Sep 13 04:47:48 2018
@@ -201,16 +201,16 @@ void ClangdServer::removeDocument(PathRe
   WorkScheduler.remove(File);
 }
 
-TaskHandle ClangdServer::codeComplete(PathRef File, Position Pos,
-                                      const clangd::CodeCompleteOptions &Opts,
-                                      Callback<CodeCompleteResult> CB) {
+Canceler ClangdServer::codeComplete(PathRef File, Position Pos,
+                                    const clangd::CodeCompleteOptions &Opts,
+                                    Callback<CodeCompleteResult> 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<PCHContainerOperations> PCHs = this->PCHs;
   auto FS = FSProvider.getFileSystem();
@@ -259,7 +259,7 @@ TaskHandle ClangdServer::codeComplete(Pa
   // 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,

Modified: clang-tools-extra/trunk/clangd/ClangdServer.h
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/ClangdServer.h?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/ClangdServer.h (original)
+++ clang-tools-extra/trunk/clangd/ClangdServer.h Thu Sep 13 04:47:48 2018
@@ -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<CodeCompleteResult> CB);
+  Canceler codeComplete(PathRef File, Position Pos,
+                        const clangd::CodeCompleteOptions &Opts,
+                        Callback<CodeCompleteResult> CB);
 
   /// Provide signature help for \p File at \p Pos.  This method should only be
   /// called for tracked files.

Modified: clang-tools-extra/trunk/unittests/clangd/CancellationTests.cpp
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/unittests/clangd/CancellationTests.cpp?rev=342130&r1=342129&r2=342130&view=diff
==============================================================================
--- clang-tools-extra/trunk/unittests/clangd/CancellationTests.cpp (original)
+++ clang-tools-extra/trunk/unittests/clangd/CancellationTests.cpp Thu Sep 13 04:47:48 2018
@@ -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<WithContext> 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<bool> 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();
 




More information about the cfe-commits mailing list