foundationdb/flow/ActorCollection.actor.cpp

276 lines
8.1 KiB
C++

/*
* ActorCollection.actor.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
*
* 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.
*/
#include "flow/ActorCollection.h"
#include "flow/IndexedSet.h"
#include "flow/UnitTest.h"
#include <boost/intrusive/list.hpp>
#include "flow/actorcompiler.h" // This must be the last #include.
class Runner final : public boost::intrusive::list_base_hook<>,
public Callback<Void>,
public FastAllocated<Runner>,
NonCopyable {
public:
Runner(PromiseStream<Runner*> complete, PromiseStream<Error> errors)
: complete(std::move(complete)), errors(std::move(errors)) {}
~Runner() { detach(); }
void start(Future<Void> task) {
if (!task.isReady()) {
registered = true;
task.addCallbackAndClear(this);
return;
}
if (task.isError()) {
error(task.getError());
} else {
fire(Void());
}
}
void fire(Void const&) override {
#ifdef ENABLE_SAMPLING
LineageReference callbackLineage = lineage;
LineageScope scope(&callbackLineage);
#endif
auto output = complete;
detach();
output.send(this);
}
void error(Error e) override {
#ifdef ENABLE_SAMPLING
LineageReference callbackLineage = lineage;
LineageScope scope(&callbackLineage);
#endif
auto output = errors;
detach();
if (e.code() != error_code_actor_cancelled) {
output.send(e);
}
}
private:
void detach() {
if (registered) {
registered = false;
Callback<Void>::remove();
}
}
PromiseStream<Runner*> complete;
PromiseStream<Error> errors;
bool registered = false;
#ifdef ENABLE_SAMPLING
LineageReference lineage = *currentLineage;
#endif
};
// An intrusive list of Runners, which are FastAllocated.
using RunnerList = boost::intrusive::list<Runner, boost::intrusive::constant_time_size<false>>;
// The runners list in the ActorCollection must be destroyed when the actor is destructed rather
// than before returning or throwing
struct RunnerListDestroyer : NonCopyable {
explicit RunnerListDestroyer(RunnerList* list) : list(list) {}
~RunnerListDestroyer() {
list->clear_and_dispose([](Runner* r) { delete r; });
}
RunnerList* list;
};
ACTOR Future<Void> actorCollection(FutureStream<Future<Void>> addActor,
int* pCount,
double* lastChangeTime,
double* idleTime,
double* allTime,
bool returnWhenEmptied) {
state RunnerList runners;
state RunnerListDestroyer runnersDestroyer(&runners);
state PromiseStream<Runner*> complete;
state PromiseStream<Error> errors;
state int count = 0;
if (!pCount)
pCount = &count;
loop choose {
when(Future<Void> f = waitNext(addActor)) {
auto runner = new Runner(complete, errors);
runners.insert(runners.end(), *runner);
runner->start(std::move(f));
++*pCount;
if (*pCount == 1 && lastChangeTime && idleTime && allTime) {
double currentTime = now();
*idleTime += currentTime - *lastChangeTime;
*allTime += currentTime - *lastChangeTime;
*lastChangeTime = currentTime;
}
}
when(Runner* runner = waitNext(complete.getFuture())) {
if (!--*pCount) {
if (lastChangeTime && idleTime && allTime) {
double currentTime = now();
*allTime += currentTime - *lastChangeTime;
*lastChangeTime = currentTime;
}
if (returnWhenEmptied)
return Void();
}
// If we didn't return then the entire list wasn't destroyed so erase/destroy runner
runners.erase_and_dispose(runners.iterator_to(*runner), [](Runner* r) { delete r; });
}
when(Error e = waitNext(errors.getFuture())) {
throw e;
}
}
}
template <class T, class U>
struct Traceable<std::pair<T, U>> {
static constexpr bool value = Traceable<T>::value && Traceable<U>::value;
static std::string toString(const std::pair<T, U>& p) {
auto tStr = Traceable<T>::toString(p.first);
auto uStr = Traceable<U>::toString(p.second);
std::string result(tStr.size() + uStr.size() + 3, 'x');
std::copy(tStr.begin(), tStr.end(), result.begin());
auto iter = result.begin() + tStr.size();
*(iter++) = ' ';
*(iter++) = '-';
*(iter++) = ' ';
std::copy(uStr.begin(), uStr.end(), iter);
return result;
}
};
void forceLinkActorCollectionTests() {}
// The above implementation relies on the behavior that fulfilling a promise
// that another when clause in the same choose block is waiting on is not fired synchronously.
TEST_CASE("/flow/actorCollection/chooseWhen") {
state Promise<Void> promise;
choose {
when(wait(delay(0))) {
promise.send(Void());
}
when(wait(promise.getFuture())) {
// Should be cancelled, since another when clause in this choose block has executed
ASSERT(false);
}
}
return Void();
}
ACTOR Future<Void> failIfNotCancelled() {
wait(delay(0));
ASSERT(false);
return Void();
}
// test contract that actors are cancelled when the actor collection is cleared
TEST_CASE("/flow/actorCollection/testCancel") {
state ActorCollection actorCollection(false);
int actors = deterministicRandom()->randomInt(1, 1000);
for (int i = 0; i < actors; i++) {
actorCollection.add(failIfNotCancelled());
}
actorCollection.clear(false);
wait(delay(0));
return Void();
}
Future<Void> failedActor() {
return operation_failed();
}
TEST_CASE("/flow/actorCollection/testReady") {
state ActorCollection actorCollection(true);
actorCollection.add(Void());
wait(actorCollection.getResult());
return Void();
}
TEST_CASE("/flow/actorCollection/testReadyWhilePending") {
state ActorCollection actorCollection(true);
state Promise<Void> pending;
actorCollection.add(pending.getFuture());
actorCollection.add(Void());
wait(delay(0));
ASSERT(!actorCollection.getResult().isReady());
pending.send(Void());
wait(actorCollection.getResult());
return Void();
}
TEST_CASE("/flow/actorCollection/testReadyError") {
state ActorCollection actorCollection(false);
actorCollection.add(failedActor());
try {
wait(actorCollection.getResult());
ASSERT(false);
} catch (Error& e) {
ASSERT_EQ(e.code(), error_code_operation_failed);
}
return Void();
}
TEST_CASE("/flow/actorCollection/testPendingErrorCancels") {
state ActorCollection actorCollection(false);
state Promise<Void> pending;
actorCollection.add(failIfNotCancelled());
actorCollection.add(pending.getFuture());
pending.sendError(operation_failed());
try {
wait(actorCollection.getResult());
ASSERT(false);
} catch (Error& e) {
ASSERT_EQ(e.code(), error_code_operation_failed);
}
wait(delay(0));
return Void();
}
// test contract that even if the actor collection has stopped and new actors are added to the promise stream, they are
// all cancelled when resetting actor
TEST_CASE("/flow/actorCollection/testCancelPromiseStream") {
state ActorCollection actorCollection(false);
int actors = deterministicRandom()->randomInt(1, 500);
for (int i = 0; i < actors; i++) {
actorCollection.add(failIfNotCancelled());
}
// this actor should cause the actorCollection actor to exit, meaning the new futures just build up in the promise
// stream
actorCollection.add(failedActor());
for (int i = 0; i < actors; i++) {
actorCollection.add(failIfNotCancelled());
}
// Instead of doing actorCollection.clear(false) we reinitialize to also clear the promise stream. Otherwise on
// resetting the actor collection actor, the new actors will be pulled from the promise stream into the new instance
// Note that this test fails on the assert in failIfNotCancelled() when this is replaced with
// actorCollection.clear(false).
actorCollection = ActorCollection(false);
wait(delay(0));
return Void();
}