Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Executors have deterministic shutdown #5547

Merged
merged 54 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
2ab887b
Remove checks for non-const references
wilhuff May 3, 2020
0e7b490
Fix typo
wilhuff May 7, 2020
2f5b8fd
Move DelayedOperation below Executor
wilhuff Apr 26, 2020
7581567
Use a common Executor::Id type
wilhuff Apr 26, 2020
832791f
Use a common Executor::TimePoint type
wilhuff May 7, 2020
5542371
Make kNoTag common
wilhuff May 7, 2020
b510bad
Harmonize Executor::TryCancel between implementations
wilhuff Apr 27, 2020
752a828
Convert ExecutorLibdispatch to use an explicit mutex
wilhuff Apr 27, 2020
6aae53e
Remove exposed Dispatch{Async,Sync} helpers
wilhuff Apr 27, 2020
0919154
Normalize delay, tag, id, operation argument ordering.
wilhuff May 7, 2020
e57f647
Remove DelayedOperation's internal notion of cancellation
wilhuff May 7, 2020
5d094d0
Make executors for testing include the test name, if available.
wilhuff May 7, 2020
65f0490
Add Task, representing a task for an Executor to execute
wilhuff May 7, 2020
a4f1272
Add Schedule, an ordered list of Tasks to execute
wilhuff May 8, 2020
31bd970
Move MakeTargetTime to task.h
wilhuff May 7, 2020
ceb3aa9
Migrate Executors to using Tasks
wilhuff May 7, 2020
f608cb0
Shorten executor test wait times
wilhuff May 8, 2020
69e8876
Fix TSAN issues
wilhuff May 8, 2020
3af0346
Make tracing facility permanently available
wilhuff May 8, 2020
bf30968
Add defer, a general facility for ad-hoc RAII.
wilhuff May 8, 2020
1fd787a
Merge branch 'wilhuff/defer' into wilhuff/deterministic-executor
wilhuff May 8, 2020
faf1d51
Fix typo
wilhuff May 8, 2020
4ad7557
Fix the IsScheduled problem in the general case
wilhuff May 8, 2020
7d7b467
Feedback 1
wilhuff May 9, 2020
1dc9e36
Only transition Task::State to kDone from kInitial
wilhuff May 9, 2020
0ba4d16
Naming
wilhuff May 9, 2020
deb6774
Moar feedback
wilhuff May 9, 2020
f29f5ad
Task::Create
wilhuff May 9, 2020
1725ecf
Cancelled
wilhuff May 9, 2020
adb51fb
Yet moar feedback
wilhuff May 9, 2020
1d87757
Review feedback
wilhuff May 12, 2020
bffaee3
Merge branch 'wilhuff/defer' into wilhuff/deterministic-executor
wilhuff May 12, 2020
c2c282d
Directly construct `Defer` from a `std::function`
wilhuff May 12, 2020
b0db78a
Merge branch 'wilhuff/defer' into wilhuff/deterministic-executor
wilhuff May 12, 2020
4f85358
More feedback:
wilhuff May 12, 2020
1182d86
Switch to uniform initial task reference counts
wilhuff May 12, 2020
167394a
Fix lowercase and cancelling
wilhuff May 12, 2020
e9ad380
Rename Complete to OnCompletion
wilhuff May 12, 2020
367c93d
Rename async_tasks_ to tasks_; also add comments
wilhuff May 12, 2020
a5766b1
auto*
wilhuff May 12, 2020
619c039
More comments
wilhuff May 12, 2020
3edbaec
Add an assertion that Release is not called more than necessary
wilhuff May 14, 2020
43eefae
Merge branch 'master' into wilhuff/deterministic-executor
wilhuff May 14, 2020
3f52fc6
Remove InverseLockGuard
wilhuff May 14, 2020
ed7d38a
Remove excess forward declaration
wilhuff May 14, 2020
8c2c9a6
Clarify ownership commentary in methods that affect it.
wilhuff May 14, 2020
48e3aea
Retain locals held outside the executor lock.
wilhuff May 14, 2020
d0ee6f4
Avoid deadlocking if the task cancels itself
wilhuff May 14, 2020
3e404a8
Correctly build on Apple platforms even if not using libdispatch
wilhuff May 14, 2020
34d7876
Fix missing lock in Schedule::Clear
wilhuff May 14, 2020
b341c17
style.sh generated changes
wilhuff May 14, 2020
786f29a
Fix definition of HAVE_LIBDISPATCH under CocoaPods
wilhuff May 15, 2020
4f6c82e
Include config.h
wilhuff May 15, 2020
f534aaa
More comments
wilhuff May 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions Firestore/core/src/util/async_queue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <utility>

#include "Firestore/core/src/util/hard_assert.h"
#include "Firestore/core/src/util/task.h"
#include "absl/algorithm/container.h"
#include "absl/memory/memory.h"

Expand Down Expand Up @@ -119,8 +120,8 @@ DelayedOperation AsyncQueue::EnqueueAfterDelay(Milliseconds delay,
delay = Milliseconds(0);
}

Executor::TaggedOperation tagged{static_cast<int>(timer_id), Wrap(operation)};
return executor_->Schedule(delay, std::move(tagged));
auto tag = static_cast<Executor::Tag>(timer_id);
return executor_->Schedule(delay, tag, Wrap(operation));
}

AsyncQueue::Operation AsyncQueue::Wrap(const Operation& operation) {
Expand Down Expand Up @@ -162,10 +163,12 @@ void AsyncQueue::RunScheduledOperationsUntil(const TimerId last_timer_id) {
"Attempted to run scheduled operations until missing timer id: %s",
last_timer_id);

for (auto next = executor_->PopFromSchedule(); next.has_value();
for (auto next = executor_->PopFromSchedule(); next != nullptr;
wilhuff marked this conversation as resolved.
Show resolved Hide resolved
next = executor_->PopFromSchedule()) {
next->operation();
if (next->tag == static_cast<int>(last_timer_id)) {
bool should_break = next->tag() == static_cast<int>(last_timer_id);
wilhuff marked this conversation as resolved.
Show resolved Hide resolved

next->Execute();
if (should_break) {
wilhuff marked this conversation as resolved.
Show resolved Hide resolved
break;
}
}
Expand Down
2 changes: 1 addition & 1 deletion Firestore/core/src/util/async_queue.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class AsyncQueue : public std::enable_shared_from_this<AsyncQueue> {
// Like `Enqueue`, but also starts the shutdown process. Once the shutdown
// process has started, calling any Enqueue* methods becomes a no-op
//
// The exception is `EnqueueEvenAfterShutdown`, operations requsted via
// The exception is `EnqueueEvenAfterShutdown`, operations requested via
// this will still be scheduled.
void EnqueueAndInitiateShutdown(const Operation& operation);

Expand Down
73 changes: 73 additions & 0 deletions Firestore/core/src/util/defer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef FIRESTORE_CORE_SRC_UTIL_DEFER_H_
#define FIRESTORE_CORE_SRC_UTIL_DEFER_H_

#include <utility>

namespace firebase {
namespace firestore {
namespace util {

template <typename Action>
class Deferred;

/**
* Creates a deferred action that will be destroyed at the close of the lexical
* scope containing the result of this function call. This is useful for
* performing ad-hoc RAII-style actions, without having to create the wrapper
* class. For example:
*
* FILE* file = fopen(filename, "rb");
* auto cleanup = defer([&] {
* if (file) {
* fclose(file);
* }
* });
*
* @param action a callable object; usually a lambda. Even if exceptions are
* enabled, when `action` is invoked it must not throw. This is similar to
* the restriction that exists on destructors generally.
*/
template <typename Action>
Deferred<Action> defer(Action&& action) {
return Deferred<Action>(std::forward<Action>(action));
}

/**
* Storage for a deferred action. The `action` is invoked during the destructor
* of the `Deferred`.
*/
template <typename Action>
class Deferred {
public:
explicit Deferred(Action&& action) : action_(std::move(action)) {
}

~Deferred() {
action_();
}

private:
Action action_;
};

} // namespace util
} // namespace firestore
} // namespace firebase

#endif // FIRESTORE_CORE_SRC_UTIL_DEFER_H_
140 changes: 83 additions & 57 deletions Firestore/core/src/util/executor.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 Google
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,46 +21,13 @@
#include <functional>
#include <memory>
#include <string>
#include <utility>

#include "absl/types/optional.h"

namespace firebase {
namespace firestore {
namespace util {

// A handle to an operation scheduled for future execution. The handle may
// outlive the operation, but it *cannot* outlive the executor that created it.
class DelayedOperation {
public:
// Creates an empty `DelayedOperation` not associated with any actual
// operation. Calling `Cancel` on it is a no-op.
DelayedOperation() {
}

// Returns whether this `DelayedOperation` is associated with an actual
// operation.
explicit operator bool() const {
return static_cast<bool>(cancel_func_);
}

// If the operation has not been run yet, cancels the operation. Otherwise,
// this function is a no-op.
void Cancel() {
if (cancel_func_) {
cancel_func_();
cancel_func_ = {};
}
}

// Internal use only.
explicit DelayedOperation(std::function<void()>&& cancel_func)
: cancel_func_{std::move(cancel_func)} {
}

private:
std::function<void()> cancel_func_;
};
class DelayedOperation;
class Task;

// An interface to a platform-specific executor of asynchronous operations
// (called tasks on other platforms).
Expand All @@ -74,22 +41,20 @@ class DelayedOperation {
// Delayed operations may be canceled if they have not already been run.
class Executor {
public:
// An opaque name for a kind of operation. All operations of the same type
// should share a tag.
using Tag = int;
static constexpr Tag kNoTag = -1;

// An opaque, monotonically increasing identifier for each operation that does
// not depend on their address. Where the `Tag` identifies the kind of
wilhuff marked this conversation as resolved.
Show resolved Hide resolved
wilhuff marked this conversation as resolved.
Show resolved Hide resolved
// operation, the `Id` identifies the specific instance.
using Id = uint32_t;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: I think it's slightly strange that Executor::Id refers to the id of an operation, not the id of the executor. On the other hand, I can understand the desire for brevity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just promoting ExecutorStd::Id to Executor (and removing the separate implementation from TimeSlot). I figured this would be noncontroversial without considering this too deeply.

If you'd like to revisit the original name from ExecutorStd I'm happy to do so, but need some ideas about what you'd find acceptable. Is this a sequence number? A task id? Should I move Id and Tag to Task? One argument against that is that nested types cannot be forward declared, which means that suddenly Task would get much wider exposure.

FWIW I'm OK with this as-is, but again, I'm happy to work to make this better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any particular preference re. moving these to Task -- this might be a little more natural, but I don't think it justifies the practical concerns you mention. I thought about simply making these Executor::TaskId/TaskTag. If you have any objection to that, I'm absolutely fine with keeping the status quo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me defer this for now. I'd like to address this naming concern along with the inability of most consumers of Executor/AsyncQueue being unable to forward declare them in a holistic fashion.

using Operation = std::function<void()>;
using Milliseconds = std::chrono::milliseconds;

// Operations scheduled for future execution have an opaque tag. The value of
// the tag is ignored by the executor but can be used to find operations with
// a given tag after they are scheduled.
struct TaggedOperation {
TaggedOperation() {
}
TaggedOperation(const Tag tag, Operation&& operation)
: tag{tag}, operation{std::move(operation)} {
}
Tag tag = 0;
Operation operation;
};
using Milliseconds = std::chrono::milliseconds;
using Clock = std::chrono::steady_clock;
using TimePoint = std::chrono::time_point<Clock, Milliseconds>;

// Creates a new serial Executor of the platform-appropriate type, and gives
// it the given label, if the implementation supports it.
Expand All @@ -113,15 +78,20 @@ class Executor {
// Like `Execute`, but blocks until the `operation` finishes, consequently
// draining immediate operations from the executor.
virtual void ExecuteBlocking(Operation&& operation) = 0;

// Scheduled the given `operation` to be executed after `delay` milliseconds
// from now, and returns a handle that allows to cancel the operation
// (provided it hasn't been run already). The operation is tagged to allow
// retrieving it later.
// (provided it hasn't been run already).
//
// Operations scheduled for future execution have an opaque tag. The value of
// the tag is ignored by the executor but can be used to find operations with
// a given tag after they are scheduled.
//
// `delay` must be non-negative; use `Execute` to schedule operations for
// immediate execution.
virtual DelayedOperation Schedule(Milliseconds delay,
TaggedOperation&& operation) = 0;
Tag tag,
Operation&& operation) = 0;

// Checks for the caller whether it is being invoked by this executor.
virtual bool IsCurrentExecutor() const = 0;
Expand All @@ -136,12 +106,68 @@ class Executor {
// Checks whether an operation tagged with the given `tag` is currently
// scheduled for future execution.
virtual bool IsScheduled(Tag tag) const = 0;
virtual bool IsTaskScheduled(Id id) const = 0;

// Removes the nearest due scheduled operation from the schedule and returns
// it to the caller. This function may be used to reschedule operations.
// Immediate operations don't count; only operations scheduled for delayed
// execution may be removed. If no such operations are currently scheduled, an
// empty `optional` is returned.
virtual absl::optional<TaggedOperation> PopFromSchedule() = 0;
// it to the caller.
//
// Only operations scheduled for delayed execution can be removed with this
// method; immediate operations don't count. If no such operations are
// currently scheduled, `nullptr` is returned.
//
// The caller is responsible for either Executing or Canceling (and Releasing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit here: Executing, etc. should be lowercase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Also canceling -> cancelling.

// the returned Task.
virtual Task* PopFromSchedule() = 0;

private:
// Mark a task completed, removing it from any internal schedule or tracking.
//
// Called by Task once it has completed execution. Implementations of
// `Complete` should not call back to the Task: it is responsible for marking
// itself completed.
virtual void Complete(Task* task) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brainstorm: from the name of this function, I would expect different semantics (like the comment seems to anticipate). To throw out some suggestions: Disown, Release, Detach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is more of an event callback I changed it to OnCompletion. I specifically want to avoid Release since that's an imperative name for something that implies a definite decrement of the reference count, but that's not happening here at all.

friend class Task;

// If the operation hasn't yet been run, it will be removed from the queue.
// Otherwise, this function is a no-op.
//
// Called by `DelayedOperation` when its user calls `Cancel`. Implementations
// of `Cancel` should also `Dispose` the underlying `Task` to actually prevent
// execution.
virtual void Cancel(Id operation_id) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, Try in the function name indicated that this function is not guaranteed to succeed (because it is unable to cancel an already started operation). This seems like something worth reminding the caller about, what is the rationale for removing it? (Note: I don't think there's any general requirement that a TryFoo function must return a boolean)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While there's no requirement that methods with a "try" prefix return a boolean they typically are designed to allow the caller to handle failure. For example, std::mutex::try_lock, clearly makes a style of programming possible wherein the caller can take action based on whether the lock was acquired or not. My take is that "try" in a method name isn't an indicator of possible failure or idempotency (because many methods have that property without being prefixed with "try"), but rather that "try" intends for the caller to discriminate. In this case I felt that the caller shouldn't care and so the "try" wasn't helpful.

Meanwhile, this was particularly apparent to me because DelayedOperation::Cancel is the actual API for cancelation used in the rest of our code. I made TryCancel private, accessible only to friends, and renamed it to match its caller.

Also FWIW, note that in google3, CancellableClosure has a Cancel even though it does allow discrimination.

friend class DelayedOperation;
};

// A handle to an operation scheduled for future execution. The handle may
// outlive the operation, but it *cannot* outlive the executor that created it.
class DelayedOperation {
public:
// Creates an empty `DelayedOperation` not associated with any actual
// operation. Calling `Cancel` on it is a no-op.
DelayedOperation() = default;

// Returns whether this `DelayedOperation` is associated with an actual
// operation.
explicit operator bool() const {
return executor_ && executor_->IsTaskScheduled(id_);
}

// If the operation has not been run yet, cancels the operation. Otherwise,
// this function is a no-op.
void Cancel() {
if (executor_) {
executor_->Cancel(id_);
}
}

// Internal use only.
explicit DelayedOperation(Executor* executor, Executor::Id id)
: executor_(executor), id_(id) {
}

private:
Executor* executor_ = nullptr;
Executor::Id id_ = 0;
};

} // namespace util
Expand Down
Loading