Refactor TaskDependencyManager, allow passing bundles of objects to ObjectManager (#13006)

* New dependency manager

* Switch raylet to new DependencyManager

* PullManager accepts bundles

* Cleanup, remove old task dependency manager

* x

* PullManager unit tests

* lint

* Unit tests

* Rename

* lint

* test

* Update src/ray/raylet/dependency_manager.cc

Co-authored-by: SangBin Cho <rkooo567@gmail.com>

* Update src/ray/raylet/dependency_manager.cc

Co-authored-by: SangBin Cho <rkooo567@gmail.com>

* x

* lint

Co-authored-by: SangBin Cho <rkooo567@gmail.com>
This commit is contained in:
Stephanie Wang
2020-12-23 18:36:00 -08:00
committed by GitHub
co-authored by SangBin Cho
parent 3cc213ddf6
commit 4461f9980a
19 changed files with 1339 additions and 1566 deletions
+2 -2
View File
@@ -956,8 +956,8 @@ cc_test(
)
cc_test(
name = "task_dependency_manager_test",
srcs = ["src/ray/raylet/task_dependency_manager_test.cc"],
name = "dependency_manager_test",
srcs = ["src/ray/raylet/dependency_manager_test.cc"],
copts = COPTS,
deps = [
":raylet_lib",
+22
View File
@@ -20,6 +20,7 @@
#include "ray/common/id.h"
#include "ray/util/logging.h"
#include "src/ray/protobuf/common.pb.h"
/// Convert an unique ID to a flatbuffer string.
///
@@ -201,3 +202,24 @@ to_flatbuf(flatbuffers::FlatBufferBuilder &fbb, const std::unordered_set<ID> &id
}
return fbb.CreateVector(results);
}
static inline ray::rpc::ObjectReference ObjectIdToRef(
const ray::ObjectID &object_id, const ray::rpc::Address owner_address) {
ray::rpc::ObjectReference ref;
ref.set_object_id(object_id.Binary());
ref.mutable_owner_address()->CopyFrom(owner_address);
return ref;
}
static inline ray::ObjectID ObjectRefToId(const ray::rpc::ObjectReference &object_ref) {
return ray::ObjectID::FromBinary(object_ref.object_id());
}
static inline std::vector<ray::ObjectID> ObjectRefsToIds(
const std::vector<ray::rpc::ObjectReference> &object_refs) {
std::vector<ray::ObjectID> object_ids;
for (const auto &ref : object_refs) {
object_ids.push_back(ObjectRefToId(ref));
}
return object_ids;
}
+27 -29
View File
@@ -103,8 +103,11 @@ ObjectManager::ObjectManager(asio::io_service &main_service, const NodeID &self_
[this](const object_manager::protocol::ObjectInfoT &object_info) {
HandleObjectAdded(object_info);
});
store_notification_->SubscribeObjDeleted(
[this](const ObjectID &oid) { NotifyDirectoryObjectDeleted(oid); });
store_notification_->SubscribeObjDeleted([this](const ObjectID &oid) {
// TODO(swang): We may want to force the pull manager to fetch this object
// again, in case it was needed by an active pull request.
NotifyDirectoryObjectDeleted(oid);
});
// Start object manager rpc server and send & receive request threads
StartRpcService();
@@ -169,10 +172,6 @@ void ObjectManager::HandleObjectAdded(
}
unfulfilled_push_requests_.erase(iter);
}
// The object is local, so we no longer need to Pull it from a remote
// manager. Cancel any outstanding Pull requests for this object.
CancelPull(object_id);
}
void ObjectManager::NotifyDirectoryObjectDeleted(const ObjectID &object_id) {
@@ -198,13 +197,9 @@ ray::Status ObjectManager::SubscribeObjDeleted(
return ray::Status::OK();
}
ray::Status ObjectManager::Pull(const ObjectID &object_id,
const rpc::Address &owner_address) {
if (!pull_manager_->Pull(object_id, owner_address)) {
// If we don't need to pull, the object is either already local or this is a duplicate
// request.
return Status::OK();
}
uint64_t ObjectManager::Pull(const std::vector<rpc::ObjectReference> &object_refs) {
std::vector<rpc::ObjectReference> objects_to_locate;
auto request_id = pull_manager_->Pull(object_refs, &objects_to_locate);
const auto &callback = [this](const ObjectID &object_id,
const std::unordered_set<NodeID> &client_ids,
@@ -212,12 +207,25 @@ ray::Status ObjectManager::Pull(const ObjectID &object_id,
pull_manager_->OnLocationChange(object_id, client_ids, spilled_url);
};
// Subscribe to object notifications. A notification will be received every
// time the set of node IDs for the object changes. Notifications will also
// be received if the list of locations is empty. The set of node IDs has
// no ordering guarantee between notifications.
return object_directory_->SubscribeObjectLocations(object_directory_pull_callback_id_,
object_id, owner_address, callback);
for (const auto &ref : objects_to_locate) {
// Subscribe to object notifications. A notification will be received every
// time the set of node IDs for the object changes. Notifications will also
// be received if the list of locations is empty. The set of node IDs has
// no ordering guarantee between notifications.
auto object_id = ObjectRefToId(ref);
RAY_CHECK_OK(object_directory_->SubscribeObjectLocations(
object_directory_pull_callback_id_, object_id, ref.owner_address(), callback));
}
return request_id;
}
void ObjectManager::CancelPull(uint64_t request_id) {
const auto objects_to_cancel = pull_manager_->CancelPull(request_id);
for (const auto &object_id : objects_to_cancel) {
RAY_CHECK_OK(object_directory_->UnsubscribeObjectLocations(
object_directory_pull_callback_id_, object_id));
}
}
void ObjectManager::SendPullRequest(const ObjectID &object_id, const NodeID &client_id) {
@@ -426,16 +434,6 @@ void ObjectManager::SendObjectChunk(const UniqueID &push_id, const ObjectID &obj
buffer_pool_.ReleaseGetChunk(object_id, chunk_info.chunk_index);
}
void ObjectManager::CancelPull(const ObjectID &object_id) {
if (!pull_manager_->CancelPull(object_id)) {
// We weren't tracking a pull request for this object, so there is nothing to cancel.
return;
}
RAY_CHECK_OK(object_directory_->UnsubscribeObjectLocations(
object_directory_pull_callback_id_, object_id));
}
ray::Status ObjectManager::Wait(
const std::vector<ObjectID> &object_ids,
const std::unordered_map<ObjectID, rpc::Address> &owner_addresses, int64_t timeout_ms,
+13 -12
View File
@@ -43,6 +43,7 @@
#include "ray/object_manager/push_manager.h"
#include "ray/rpc/object_manager/object_manager_client.h"
#include "ray/rpc/object_manager/object_manager_server.h"
#include "src/ray/protobuf/common.pb.h"
namespace ray {
@@ -95,9 +96,8 @@ class ObjectStoreRunner {
class ObjectManagerInterface {
public:
virtual ray::Status Pull(const ObjectID &object_id,
const rpc::Address &owner_address) = 0;
virtual void CancelPull(const ObjectID &object_id) = 0;
virtual uint64_t Pull(const std::vector<rpc::ObjectReference> &object_refs) = 0;
virtual void CancelPull(uint64_t request_id) = 0;
virtual ~ObjectManagerInterface(){};
};
@@ -238,18 +238,19 @@ class ObjectManager : public ObjectManagerInterface,
/// \return Void.
void Push(const ObjectID &object_id, const NodeID &node_id);
/// Pull an object from NodeID.
/// Pull a bundle of objects. This will attempt to make all objects in the
/// bundle local until the request is canceled with the returned ID.
///
/// \param object_id The object's object id.
/// \return Status of whether the pull request successfully initiated.
ray::Status Pull(const ObjectID &object_id, const rpc::Address &owner_address) override;
/// \param object_refs The bundle of objects that must be made local.
/// \return A request ID that can be used to cancel the request.
uint64_t Pull(const std::vector<rpc::ObjectReference> &object_refs) override;
/// Cancels all requests (Push/Pull) associated with the given ObjectID. This
/// method is idempotent.
/// Cancels the pull request with the given ID. This cancels any fetches for
/// objects that were passed to the original pull request, if no other pull
/// request requires them.
///
/// \param object_id The ObjectID.
/// \return Void.
void CancelPull(const ObjectID &object_id) override;
/// \param pull_request_id The request to cancel.
void CancelPull(uint64_t pull_request_id) override;
/// Callback definition for wait.
using WaitCallback = std::function<void(const std::vector<ray::ObjectID> &found,
+78 -43
View File
@@ -1,5 +1,7 @@
#include "ray/object_manager/pull_manager.h"
#include "ray/common/common_protocol.h"
namespace ray {
PullManager::PullManager(
@@ -15,29 +17,56 @@ PullManager::PullManager(
pull_timeout_ms_(pull_timeout_ms),
gen_(std::chrono::high_resolution_clock::now().time_since_epoch().count()) {}
bool PullManager::Pull(const ObjectID &object_id, const rpc::Address &owner_address) {
RAY_LOG(DEBUG) << "Pull "
<< " of object " << object_id;
// Check if object is already local.
if (object_is_local_(object_id)) {
RAY_LOG(DEBUG) << object_id << " attempted to pull an object that's already local.";
return false;
}
if (pull_requests_.find(object_id) != pull_requests_.end()) {
RAY_LOG(DEBUG) << object_id << " has inflight pull_requests, skipping.";
return false;
uint64_t PullManager::Pull(const std::vector<rpc::ObjectReference> &object_ref_bundle,
std::vector<rpc::ObjectReference> *objects_to_locate) {
auto bundle_it = pull_request_bundles_.emplace(next_req_id_++, object_ref_bundle).first;
RAY_LOG(DEBUG) << "Start pull request " << bundle_it->first;
for (const auto &ref : object_ref_bundle) {
auto obj_id = ObjectRefToId(ref);
auto it = object_pull_requests_.find(obj_id);
if (it == object_pull_requests_.end()) {
RAY_LOG(DEBUG) << "Pull of object " << obj_id;
// We don't have an active pull for this object yet. Ask the caller to
// send us notifications about the object's location.
objects_to_locate->push_back(ref);
it = object_pull_requests_
.emplace(obj_id, ObjectPullRequest(get_time_() + pull_timeout_ms_ / 1000))
.first;
}
it->second.bundle_request_ids.insert(bundle_it->first);
}
pull_requests_.emplace(object_id, PullRequest(get_time_() + pull_timeout_ms_ / 1000));
return true;
return bundle_it->first;
}
std::vector<ObjectID> PullManager::CancelPull(uint64_t request_id) {
std::vector<ObjectID> objects_to_cancel;
RAY_LOG(DEBUG) << "Cancel pull request " << request_id;
auto bundle_it = pull_request_bundles_.find(request_id);
RAY_CHECK(bundle_it != pull_request_bundles_.end());
for (const auto &ref : bundle_it->second) {
auto obj_id = ObjectRefToId(ref);
auto it = object_pull_requests_.find(obj_id);
RAY_CHECK(it != object_pull_requests_.end());
RAY_CHECK(it->second.bundle_request_ids.erase(request_id));
if (it->second.bundle_request_ids.empty()) {
object_pull_requests_.erase(it);
objects_to_cancel.push_back(obj_id);
}
}
pull_request_bundles_.erase(bundle_it);
return objects_to_cancel;
}
void PullManager::OnLocationChange(const ObjectID &object_id,
const std::unordered_set<NodeID> &client_ids,
const std::string &spilled_url) {
// Exit if the Pull request has already been fulfilled or canceled.
auto it = pull_requests_.find(object_id);
if (it == pull_requests_.end()) {
auto it = object_pull_requests_.find(object_id);
if (it == object_pull_requests_.end()) {
return;
}
// Reset the list of clients that are now expected to have the object.
@@ -45,28 +74,50 @@ void PullManager::OnLocationChange(const ObjectID &object_id,
// we may end up sending a duplicate request to the same client as
// before.
it->second.client_locations = std::vector<NodeID>(client_ids.begin(), client_ids.end());
if (!spilled_url.empty()) {
RAY_LOG(DEBUG) << "OnLocationChange " << spilled_url << " num clients "
<< client_ids.size();
it->second.spilled_url = spilled_url;
RAY_LOG(DEBUG) << "OnLocationChange " << spilled_url << " num clients "
<< client_ids.size();
TryToMakeObjectLocal(object_id);
}
void PullManager::TryToMakeObjectLocal(const ObjectID &object_id) {
if (object_is_local_(object_id)) {
return;
}
auto it = object_pull_requests_.find(object_id);
if (it == object_pull_requests_.end()) {
return;
}
auto &request = it->second;
if (!request.spilled_url.empty()) {
// Try to restore the spilled object.
restore_spilled_object_(object_id, spilled_url,
restore_spilled_object_(object_id, request.spilled_url,
[this, object_id](const ray::Status &status) {
// Fall back to fetching from another object manager.
if (!status.ok()) {
TryPull(object_id);
PullFromRandomLocation(object_id);
}
});
} else {
// New object locations were found, so begin trying to pull from a
// client. This will be called every time a new client location
// appears.
TryPull(object_id);
PullFromRandomLocation(object_id);
}
const auto time = get_time_();
auto retry_timeout_len = (pull_timeout_ms_ / 1000.) * (1UL << request.num_retries);
request.next_pull_time = time + retry_timeout_len;
// Bound the retry time at 10 * 1024 seconds.
request.num_retries = std::min(request.num_retries + 1, 10);
}
void PullManager::TryPull(const ObjectID &object_id) {
auto it = pull_requests_.find(object_id);
if (it == pull_requests_.end()) {
void PullManager::PullFromRandomLocation(const ObjectID &object_id) {
auto it = object_pull_requests_.find(object_id);
if (it == object_pull_requests_.end()) {
return;
}
@@ -111,36 +162,20 @@ void PullManager::TryPull(const ObjectID &object_id) {
RAY_LOG(DEBUG) << "Sending pull request from " << self_node_id_ << " to " << node_id
<< " of object " << object_id;
const auto time = get_time_();
auto &request = it->second;
auto retry_timeout_len = (pull_timeout_ms_ / 1000.) * (1UL << request.num_retries);
request.next_pull_time = time + retry_timeout_len;
send_pull_request_(object_id, node_id);
}
bool PullManager::CancelPull(const ObjectID &object_id) {
auto it = pull_requests_.find(object_id);
if (it == pull_requests_.end()) {
return false;
}
pull_requests_.erase(it);
return true;
}
void PullManager::Tick() {
for (auto &pair : pull_requests_) {
for (auto &pair : object_pull_requests_) {
const auto &object_id = pair.first;
auto &request = pair.second;
const auto time = get_time_();
if (time >= request.next_pull_time) {
TryPull(object_id);
// Bound the retry time at 10 * 1024 seconds.
request.num_retries = std::min(request.num_retries + 1, 10);
TryToMakeObjectLocal(object_id);
}
}
}
int PullManager::NumActiveRequests() const { return pull_requests_.size(); }
int PullManager::NumActiveRequests() const { return object_pull_requests_.size(); }
} // namespace ray
+43 -31
View File
@@ -42,33 +42,32 @@ class PullManager {
const RestoreSpilledObjectCallback restore_spilled_object,
const std::function<double()> get_time, int pull_timeout_ms);
/// Begin a new pull request if necessary.
/// Begin a new pull request for a bundle of objects.
///
/// \param object_id The object id to pull.
/// \param owner_address The owner of the object.
///
/// \return True if a new pull request was necessary. If true, the caller should
/// subscribe to new locations of the object, and call OnLocationChange when necessary.
bool Pull(const ObjectID &object_id, const rpc::Address &owner_address);
/// \param object_refs The bundle of objects that must be made local.
/// \param objects_to_locate The objects whose new locations the caller
/// should subscribe to, and call OnLocationChange for.
/// \return A request ID that can be used to cancel the request.
uint64_t Pull(const std::vector<rpc::ObjectReference> &object_ref_bundle,
std::vector<rpc::ObjectReference> *objects_to_locate);
/// Called when the available locations for a given object change.
///
/// \param object_id The ID of the object which is now available in a new location.
/// \param client_ids The new set of nodes that the object is available on. Not
/// necessarily a super or subset of the previously available nodes. \param spilled_url
/// The location of the object if it was spilled. If non-empty, the object may no longer
/// be on any node.
/// necessarily a super or subset of the previously available nodes.
/// \param spilled_url The location of the object if it was spilled. If
/// non-empty, the object may no longer be on any node.
void OnLocationChange(const ObjectID &object_id,
const std::unordered_set<NodeID> &client_ids,
const std::string &spilled_url);
/// Cancel an existing pull request if necessary.
/// Cancel an existing pull request.
///
/// \param object_id The object id that no longer needs to be pulled.
///
/// \return True if a pull was cancelled. If there was no pending pull request for the
/// object this method may return false.
bool CancelPull(const ObjectID &object_id);
/// \param request_id The request ID returned by Pull that should be canceled.
/// \return The objects for which the caller should stop subscribing to
/// locations.
std::vector<ObjectID> CancelPull(uint64_t request_id);
/// Called when the retry timer fires. If this fires, the pull manager may try to pull
/// existing objects from other nodes if necessary.
@@ -79,14 +78,32 @@ class PullManager {
private:
/// A helper structure for tracking information about each ongoing object pull.
struct PullRequest {
PullRequest(double first_retry_time)
: client_locations(), next_pull_time(first_retry_time), num_retries(0) {}
struct ObjectPullRequest {
ObjectPullRequest(double first_retry_time)
: client_locations(),
spilled_url(),
next_pull_time(first_retry_time),
num_retries(0),
bundle_request_ids() {}
std::vector<NodeID> client_locations;
std::string spilled_url;
double next_pull_time;
uint8_t num_retries;
absl::flat_hash_set<uint64_t> bundle_request_ids;
};
/// Try to make an object local, by restoring the object from external
/// storage or by fetching the object from one of its expected client
/// locations. This does nothing if the object is not needed by any pull
/// request or if it is already local. This also sets a timeout for when to
/// make the next attempt to make the object local.
void TryToMakeObjectLocal(const ObjectID &object_id);
/// Try to Pull an object from one of its expected client locations. If there
/// are more client locations to try after this attempt, then this method
/// will try each of the other clients in succession.
void PullFromRandomLocation(const ObjectID &object_id);
/// See the constructor's arguments.
NodeID self_node_id_;
const std::function<bool(const ObjectID &)> object_is_local_;
@@ -95,22 +112,17 @@ class PullManager {
const std::function<double()> get_time_;
uint64_t pull_timeout_ms_;
/// The next ID to assign to a bundle pull request, so that the caller can
/// cancel. Start at 1 because 0 means null.
uint64_t next_req_id_ = 1;
std::unordered_map<uint64_t, std::vector<rpc::ObjectReference>> pull_request_bundles_;
/// The objects that this object manager is currently trying to fetch from
/// remote object managers.
std::unordered_map<ObjectID, PullRequest> pull_requests_;
std::unordered_map<ObjectID, ObjectPullRequest> object_pull_requests_;
/// Internally maintained random number generator.
std::mt19937_64 gen_;
/// Try to Pull an object from one of its expected client locations. If there
/// are more client locations to try after this attempt, then this method
/// will try each of the other clients in succession, with a timeout between
/// each attempt. If the object is received or if the Pull is Canceled before
/// the timeout, then no more Pull requests for this object will be sent
/// to other node managers until TryPull is called again.
///
/// \param object_id The object's object id.
/// \return Void.
void TryPull(const ObjectID &object_id);
};
} // namespace ray
@@ -18,6 +18,7 @@
#include <thread>
#include "gtest/gtest.h"
#include "ray/common/common_protocol.h"
#include "ray/common/status.h"
#include "ray/common/test_util.h"
#include "ray/gcs/gcs_client/service_based_gcs_client.h"
@@ -338,8 +339,6 @@ class StressTestObjectManager : public TestObjectManagerBase {
NodeID node_id_1 = gcs_client_1->Nodes().GetSelfId();
NodeID node_id_2 = gcs_client_2->Nodes().GetSelfId();
ray::Status status = ray::Status::OK();
if (transfer_pattern == TransferPattern::BIDIRECTIONAL_PULL ||
transfer_pattern == TransferPattern::BIDIRECTIONAL_PUSH ||
transfer_pattern == TransferPattern::BIDIRECTIONAL_PULL_VARIABLE_DATA_SIZE) {
@@ -374,21 +373,25 @@ class StressTestObjectManager : public TestObjectManagerBase {
case TransferPattern::PULL_A_B: {
for (int i = -1; ++i < num_trials;) {
ObjectID oid1 = WriteDataToClient(client1, data_size);
status = server2->object_manager_.Pull(oid1, rpc::Address());
static_cast<void>(
server2->object_manager_.Pull({ObjectIdToRef(oid1, rpc::Address())}));
}
} break;
case TransferPattern::PULL_B_A: {
for (int i = -1; ++i < num_trials;) {
ObjectID oid2 = WriteDataToClient(client2, data_size);
status = server1->object_manager_.Pull(oid2, rpc::Address());
static_cast<void>(
server1->object_manager_.Pull({ObjectIdToRef(oid2, rpc::Address())}));
}
} break;
case TransferPattern::BIDIRECTIONAL_PULL: {
for (int i = -1; ++i < num_trials;) {
ObjectID oid1 = WriteDataToClient(client1, data_size);
status = server2->object_manager_.Pull(oid1, rpc::Address());
static_cast<void>(
server2->object_manager_.Pull({ObjectIdToRef(oid1, rpc::Address())}));
ObjectID oid2 = WriteDataToClient(client2, data_size);
status = server1->object_manager_.Pull(oid2, rpc::Address());
static_cast<void>(
server1->object_manager_.Pull({ObjectIdToRef(oid2, rpc::Address())}));
}
} break;
case TransferPattern::BIDIRECTIONAL_PULL_VARIABLE_DATA_SIZE: {
@@ -397,9 +400,11 @@ class StressTestObjectManager : public TestObjectManagerBase {
std::uniform_int_distribution<> dis(1, 50);
for (int i = -1; ++i < num_trials;) {
ObjectID oid1 = WriteDataToClient(client1, data_size + dis(gen));
status = server2->object_manager_.Pull(oid1, rpc::Address());
static_cast<void>(
server2->object_manager_.Pull({ObjectIdToRef(oid1, rpc::Address())}));
ObjectID oid2 = WriteDataToClient(client2, data_size + dis(gen));
status = server1->object_manager_.Pull(oid2, rpc::Address());
static_cast<void>(
server1->object_manager_.Pull({ObjectIdToRef(oid2, rpc::Address())}));
}
} break;
default: {
+134 -23
View File
@@ -1,11 +1,15 @@
#include "ray/object_manager/pull_manager.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ray/common/common_protocol.h"
#include "ray/common/test_util.h"
namespace ray {
using ::testing::ElementsAre;
class PullManagerTest : public ::testing::Test {
public:
PullManagerTest()
@@ -33,28 +37,42 @@ class PullManagerTest : public ::testing::Test {
PullManager pull_manager_;
};
std::vector<rpc::ObjectReference> CreateObjectRefs(int num_objs) {
std::vector<rpc::ObjectReference> refs;
for (int i = 0; i < num_objs; i++) {
ObjectID obj = ObjectID::FromRandom();
rpc::ObjectReference ref;
ref.set_object_id(obj.Binary());
refs.push_back(ref);
}
return refs;
}
TEST_F(PullManagerTest, TestStaleSubscription) {
ObjectID obj1 = ObjectID::FromRandom();
rpc::Address addr1;
auto refs = CreateObjectRefs(1);
auto oid = ObjectRefsToIds(refs)[0];
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
pull_manager_.Pull(obj1, addr1);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 1);
std::unordered_set<NodeID> client_ids;
pull_manager_.OnLocationChange(obj1, client_ids, "");
pull_manager_.OnLocationChange(oid, client_ids, "");
// There are no client ids to pull from.
ASSERT_EQ(num_send_pull_request_calls_, 0);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
pull_manager_.CancelPull(obj1);
auto objects_to_cancel = pull_manager_.CancelPull(req_id);
ASSERT_EQ(objects_to_cancel, ObjectRefsToIds(refs));
ASSERT_EQ(num_send_pull_request_calls_, 0);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
client_ids.insert(NodeID::FromRandom());
pull_manager_.OnLocationChange(obj1, client_ids, "");
pull_manager_.OnLocationChange(oid, client_ids, "");
// Now we're getting a notification about an object that was already cancelled.
ASSERT_EQ(num_send_pull_request_calls_, 0);
@@ -63,10 +81,13 @@ TEST_F(PullManagerTest, TestStaleSubscription) {
}
TEST_F(PullManagerTest, TestRestoreSpilledObject) {
ObjectID obj1 = ObjectID::FromRandom();
auto refs = CreateObjectRefs(1);
auto obj1 = ObjectRefsToIds(refs)[0];
rpc::Address addr1;
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
pull_manager_.Pull(obj1, addr1);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 1);
std::unordered_set<NodeID> client_ids;
@@ -84,15 +105,25 @@ TEST_F(PullManagerTest, TestRestoreSpilledObject) {
ASSERT_EQ(num_send_pull_request_calls_, 0);
ASSERT_EQ(num_restore_spilled_object_calls_, 2);
pull_manager_.CancelPull(obj1);
// Don't restore an object if it's local.
object_is_local_ = true;
num_restore_spilled_object_calls_ = 0;
pull_manager_.OnLocationChange(obj1, client_ids, "remote_url/foo/bar");
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
auto objects_to_cancel = pull_manager_.CancelPull(req_id);
ASSERT_EQ(objects_to_cancel, ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
}
TEST_F(PullManagerTest, TestManyUpdates) {
ObjectID obj1 = ObjectID::FromRandom();
auto refs = CreateObjectRefs(1);
auto obj1 = ObjectRefsToIds(refs)[0];
rpc::Address addr1;
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
pull_manager_.Pull(obj1, addr1);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 1);
std::unordered_set<NodeID> client_ids;
@@ -105,15 +136,19 @@ TEST_F(PullManagerTest, TestManyUpdates) {
ASSERT_EQ(num_send_pull_request_calls_, 100);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
pull_manager_.CancelPull(obj1);
auto objects_to_cancel = pull_manager_.CancelPull(req_id);
ASSERT_EQ(objects_to_cancel, ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
}
TEST_F(PullManagerTest, TestRetryTimer) {
ObjectID obj1 = ObjectID::FromRandom();
auto refs = CreateObjectRefs(1);
auto obj1 = ObjectRefsToIds(refs)[0];
rpc::Address addr1;
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
pull_manager_.Pull(obj1, addr1);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 1);
std::unordered_set<NodeID> client_ids;
@@ -143,26 +178,102 @@ TEST_F(PullManagerTest, TestRetryTimer) {
ASSERT_EQ(num_send_pull_request_calls_, 1 + 7 + 127);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
pull_manager_.CancelPull(obj1);
// Don't retry an object if it's local.
object_is_local_ = true;
num_send_pull_request_calls_ = 0;
for (; fake_time_ <= 127 * 10; fake_time_ += 1.) {
pull_manager_.Tick();
}
ASSERT_EQ(num_send_pull_request_calls_, 0);
auto objects_to_cancel = pull_manager_.CancelPull(req_id);
ASSERT_EQ(objects_to_cancel, ObjectRefsToIds(refs));
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
}
TEST_F(PullManagerTest, TestBasic) {
ObjectID obj1 = ObjectID::FromRandom();
rpc::Address addr1;
auto refs = CreateObjectRefs(3);
auto oids = ObjectRefsToIds(refs);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
pull_manager_.Pull(obj1, addr1);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 1);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), oids);
ASSERT_EQ(pull_manager_.NumActiveRequests(), oids.size());
std::unordered_set<NodeID> client_ids;
client_ids.insert(NodeID::FromRandom());
pull_manager_.OnLocationChange(obj1, client_ids, "");
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
ASSERT_EQ(num_send_pull_request_calls_, i + 1);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
}
ASSERT_EQ(num_send_pull_request_calls_, 1);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
// Don't pull an object if it's local.
object_is_local_ = true;
num_send_pull_request_calls_ = 0;
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
}
ASSERT_EQ(num_send_pull_request_calls_, 0);
pull_manager_.CancelPull(obj1);
auto objects_to_cancel = pull_manager_.CancelPull(req_id);
ASSERT_EQ(objects_to_cancel, oids);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
// Don't pull a remote object if we've canceled.
object_is_local_ = false;
num_send_pull_request_calls_ = 0;
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
}
ASSERT_EQ(num_send_pull_request_calls_, 0);
}
TEST_F(PullManagerTest, TestDeduplicateBundles) {
auto refs = CreateObjectRefs(3);
auto oids = ObjectRefsToIds(refs);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
std::vector<rpc::ObjectReference> objects_to_locate;
auto req_id1 = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_EQ(ObjectRefsToIds(objects_to_locate), oids);
ASSERT_EQ(pull_manager_.NumActiveRequests(), oids.size());
objects_to_locate.clear();
auto req_id2 = pull_manager_.Pull(refs, &objects_to_locate);
ASSERT_TRUE(objects_to_locate.empty());
std::unordered_set<NodeID> client_ids;
client_ids.insert(NodeID::FromRandom());
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
ASSERT_EQ(num_send_pull_request_calls_, i + 1);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
}
// Cancel one request.
auto objects_to_cancel = pull_manager_.CancelPull(req_id1);
ASSERT_TRUE(objects_to_cancel.empty());
// Objects should still be pulled because the other request is still open.
ASSERT_EQ(pull_manager_.NumActiveRequests(), oids.size());
num_send_pull_request_calls_ = 0;
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
ASSERT_EQ(num_send_pull_request_calls_, i + 1);
ASSERT_EQ(num_restore_spilled_object_calls_, 0);
}
// Cancel the other request.
objects_to_cancel = pull_manager_.CancelPull(req_id2);
ASSERT_EQ(objects_to_cancel, oids);
ASSERT_EQ(pull_manager_.NumActiveRequests(), 0);
// Don't pull a remote object if we've canceled.
object_is_local_ = false;
num_send_pull_request_calls_ = 0;
for (size_t i = 0; i < oids.size(); i++) {
pull_manager_.OnLocationChange(oids[i], client_ids, "");
}
ASSERT_EQ(num_send_pull_request_calls_, 0);
}
} // namespace ray
+311
View File
@@ -0,0 +1,311 @@
#include "ray/raylet/dependency_manager.h"
namespace ray {
namespace raylet {
bool DependencyManager::CheckObjectLocal(const ObjectID &object_id) const {
return local_objects_.count(object_id) == 1;
}
bool DependencyManager::GetOwnerAddress(const ObjectID &object_id,
rpc::Address *owner_address) const {
auto obj = required_objects_.find(object_id);
if (obj == required_objects_.end()) {
return false;
}
*owner_address = obj->second.owner_address;
return !owner_address->worker_id().empty();
}
void DependencyManager::RemoveObjectIfNotNeeded(
absl::flat_hash_map<ObjectID, DependencyManager::ObjectDependencies>::iterator
required_object_it) {
const auto &object_id = required_object_it->first;
if (required_object_it->second.Empty()) {
RAY_LOG(DEBUG) << "Object " << object_id << " no longer needed";
if (required_object_it->second.wait_request_id > 0) {
RAY_LOG(DEBUG) << "Canceling pull for wait request of object " << object_id
<< " request: " << required_object_it->second.wait_request_id;
object_manager_.CancelPull(required_object_it->second.wait_request_id);
}
if (!local_objects_.count(object_id)) {
reconstruction_policy_.Cancel(object_id);
}
required_objects_.erase(required_object_it);
}
}
absl::flat_hash_map<ObjectID, DependencyManager::ObjectDependencies>::iterator
DependencyManager::GetOrInsertRequiredObject(const ObjectID &object_id,
const rpc::ObjectReference &ref) {
auto it = required_objects_.find(object_id);
if (it == required_objects_.end()) {
it = required_objects_.emplace(object_id, ref).first;
if (local_objects_.count(object_id) == 0) {
reconstruction_policy_.ListenAndMaybeReconstruct(object_id, ref.owner_address());
}
}
return it;
}
void DependencyManager::StartOrUpdateWaitRequest(
const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects) {
RAY_LOG(DEBUG) << "Starting wait request for worker " << worker_id;
auto &wait_request = wait_requests_[worker_id];
for (const auto &ref : required_objects) {
const auto obj_id = ObjectRefToId(ref);
if (local_objects_.count(obj_id)) {
// Object is already local. No need to fetch it.
continue;
}
if (wait_request.insert(obj_id).second) {
RAY_LOG(DEBUG) << "Worker " << worker_id << " called ray.wait on non-local object "
<< obj_id;
auto it = GetOrInsertRequiredObject(obj_id, ref);
it->second.dependent_wait_requests.insert(worker_id);
if (it->second.wait_request_id == 0) {
it->second.wait_request_id = object_manager_.Pull({ref});
RAY_LOG(DEBUG) << "Started pull for wait request for object " << obj_id
<< " request: " << it->second.wait_request_id;
}
}
}
// No new objects to wait on. Delete the empty entry that was created.
if (wait_request.empty()) {
wait_requests_.erase(worker_id);
}
}
void DependencyManager::CancelWaitRequest(const WorkerID &worker_id) {
RAY_LOG(DEBUG) << "Canceling wait request for worker " << worker_id;
auto it = wait_requests_.find(worker_id);
if (it == wait_requests_.end()) {
return;
}
for (const auto &obj_id : it->second) {
auto it = required_objects_.find(obj_id);
RAY_CHECK(it != required_objects_.end());
it->second.dependent_wait_requests.erase(worker_id);
RemoveObjectIfNotNeeded(it);
}
wait_requests_.erase(it);
}
void DependencyManager::StartOrUpdateGetRequest(
const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects) {
RAY_LOG(DEBUG) << "Starting get request for worker " << worker_id;
auto &get_request = get_requests_[worker_id];
bool modified = false;
for (const auto &ref : required_objects) {
const auto obj_id = ObjectRefToId(ref);
if (get_request.first.insert(obj_id).second) {
RAY_LOG(DEBUG) << "Worker " << worker_id << " called ray.get on object " << obj_id;
auto it = GetOrInsertRequiredObject(obj_id, ref);
it->second.dependent_get_requests.insert(worker_id);
modified = true;
}
}
if (modified) {
std::vector<rpc::ObjectReference> refs;
for (auto &obj_id : get_request.first) {
auto it = required_objects_.find(obj_id);
RAY_CHECK(it != required_objects_.end());
refs.push_back(ObjectIdToRef(obj_id, it->second.owner_address));
}
// Pull the new dependencies before canceling the old request, in case some
// of the old dependencies are still being fetched.
uint64_t new_request_id = object_manager_.Pull(refs);
if (get_request.second != 0) {
RAY_LOG(DEBUG) << "Canceling pull for get request from worker " << worker_id
<< " request: " << get_request.second;
object_manager_.CancelPull(get_request.second);
}
get_request.second = new_request_id;
RAY_LOG(DEBUG) << "Started pull for get request from worker " << worker_id
<< " request: " << get_request.second;
}
}
void DependencyManager::CancelGetRequest(const WorkerID &worker_id) {
RAY_LOG(DEBUG) << "Canceling get request for worker " << worker_id;
auto it = get_requests_.find(worker_id);
if (it == get_requests_.end()) {
return;
}
RAY_LOG(DEBUG) << "Canceling pull for get request from worker " << worker_id
<< " request: " << it->second.second;
object_manager_.CancelPull(it->second.second);
for (const auto &obj_id : it->second.first) {
auto it = required_objects_.find(obj_id);
RAY_CHECK(it != required_objects_.end());
it->second.dependent_get_requests.erase(worker_id);
RemoveObjectIfNotNeeded(it);
}
get_requests_.erase(it);
}
/// Request dependencies for a queued task.
bool DependencyManager::RequestTaskDependencies(
const TaskID &task_id, const std::vector<rpc::ObjectReference> &required_objects) {
RAY_LOG(DEBUG) << "Adding dependencies for task " << task_id;
auto inserted = queued_task_requests_.emplace(task_id, required_objects);
RAY_CHECK(inserted.second) << "Task depedencies can be requested only once per task.";
auto &task_entry = inserted.first->second;
for (const auto &ref : required_objects) {
const auto obj_id = ObjectRefToId(ref);
RAY_LOG(DEBUG) << "Task " << task_id << " blocked on object " << obj_id;
auto it = GetOrInsertRequiredObject(obj_id, ref);
it->second.dependent_tasks.insert(task_id);
if (local_objects_.count(obj_id)) {
task_entry.num_missing_dependencies--;
}
}
if (!required_objects.empty()) {
task_entry.pull_request_id = object_manager_.Pull(required_objects);
RAY_LOG(DEBUG) << "Started pull for dependencies of task " << task_id
<< " request: " << task_entry.pull_request_id;
}
return task_entry.num_missing_dependencies == 0;
}
bool DependencyManager::IsTaskReady(const TaskID &task_id) const {
auto task_entry = queued_task_requests_.find(task_id);
RAY_CHECK(task_entry != queued_task_requests_.end());
return task_entry->second.num_missing_dependencies == 0;
}
void DependencyManager::RemoveTaskDependencies(const TaskID &task_id) {
RAY_LOG(DEBUG) << "Removing dependencies for task " << task_id;
auto task_entry = queued_task_requests_.find(task_id);
RAY_CHECK(task_entry != queued_task_requests_.end())
<< "Can't remove dependencies of tasks that are not queued.";
if (task_entry->second.pull_request_id > 0) {
RAY_LOG(DEBUG) << "Canceling pull for dependencies of task " << task_id
<< " request: " << task_entry->second.pull_request_id;
object_manager_.CancelPull(task_entry->second.pull_request_id);
}
for (const auto &obj_id : task_entry->second.dependencies) {
auto it = required_objects_.find(obj_id);
RAY_CHECK(it != required_objects_.end());
it->second.dependent_tasks.erase(task_id);
RemoveObjectIfNotNeeded(it);
}
queued_task_requests_.erase(task_entry);
}
std::vector<TaskID> DependencyManager::HandleObjectMissing(
const ray::ObjectID &object_id) {
RAY_CHECK(local_objects_.erase(object_id))
<< "Evicted object was not local " << object_id;
// Find any tasks that are dependent on the missing object.
std::vector<TaskID> waiting_task_ids;
auto object_entry = required_objects_.find(object_id);
if (object_entry != required_objects_.end()) {
for (auto &dependent_task_id : object_entry->second.dependent_tasks) {
auto it = queued_task_requests_.find(dependent_task_id);
RAY_CHECK(it != queued_task_requests_.end());
auto &task_entry = it->second;
// If the dependent task had all of its arguments ready, it was ready to
// run but must be switched to waiting since one of its arguments is now
// missing.
if (task_entry.num_missing_dependencies == 0) {
waiting_task_ids.push_back(dependent_task_id);
// During normal execution we should be able to include the check
// RAY_CHECK(pending_tasks_.count(dependent_task_id) == 1);
// However, this invariant will not hold during unit test execution.
}
task_entry.num_missing_dependencies++;
}
// The object is missing and needed so wait for a possible failure again.
reconstruction_policy_.ListenAndMaybeReconstruct(object_entry->first,
object_entry->second.owner_address);
}
// Process callbacks for all of the tasks dependent on the object that are
// now ready to run.
return waiting_task_ids;
}
std::vector<TaskID> DependencyManager::HandleObjectLocal(const ray::ObjectID &object_id) {
// Add the object to the table of locally available objects.
auto inserted = local_objects_.insert(object_id);
RAY_CHECK(inserted.second) << "Local object was already local " << object_id;
// Find all tasks and workers that depend on the newly available object.
std::vector<TaskID> ready_task_ids;
auto object_entry = required_objects_.find(object_id);
if (object_entry != required_objects_.end()) {
// Loop through all tasks that depend on the newly available object.
for (const auto &dependent_task_id : object_entry->second.dependent_tasks) {
auto it = queued_task_requests_.find(dependent_task_id);
RAY_CHECK(it != queued_task_requests_.end());
auto &task_entry = it->second;
task_entry.num_missing_dependencies--;
// If the dependent task now has all of its arguments ready, it's ready
// to run.
if (task_entry.num_missing_dependencies == 0) {
ready_task_ids.push_back(dependent_task_id);
}
}
// Remove the dependency from all workers that called `ray.wait` on the
// newly available object.
for (const auto &worker_id : object_entry->second.dependent_wait_requests) {
auto worker_it = wait_requests_.find(worker_id);
RAY_CHECK(worker_it != wait_requests_.end());
RAY_CHECK(worker_it->second.erase(object_id) > 0);
if (worker_it->second.empty()) {
wait_requests_.erase(worker_it);
}
}
// Clear all workers that called `ray.wait` on this object, since the
// `ray.wait` calls can now return the object as ready.
object_entry->second.dependent_wait_requests.clear();
if (object_entry->second.wait_request_id > 0) {
RAY_LOG(DEBUG) << "Canceling pull for wait request of object " << object_id
<< " request: " << object_entry->second.wait_request_id;
object_manager_.CancelPull(object_entry->second.wait_request_id);
object_entry->second.wait_request_id = 0;
}
reconstruction_policy_.Cancel(object_entry->first);
RemoveObjectIfNotNeeded(object_entry);
}
return ready_task_ids;
}
std::string DependencyManager::DebugString() const {
std::stringstream result;
result << "TaskDependencyManager:";
result << "\n- task deps map size: " << queued_task_requests_.size();
result << "\n- get req map size: " << get_requests_.size();
result << "\n- wait req map size: " << wait_requests_.size();
result << "\n- local objects map size: " << local_objects_.size();
return result.str();
}
} // namespace raylet
} // namespace ray
+270
View File
@@ -0,0 +1,270 @@
// Copyright 2017 The Ray 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.
#pragma once
// clang-format off
#include "ray/common/common_protocol.h"
#include "ray/common/id.h"
#include "ray/common/task/task.h"
#include "ray/object_manager/object_manager.h"
#include "ray/raylet/reconstruction_policy.h"
// clang-format on
namespace ray {
namespace raylet {
using rpc::TaskLeaseData;
class ReconstructionPolicy;
/// Used for unit-testing the ClusterTaskManager, which requests dependencies
/// for queued tasks.
class TaskDependencyManagerInterface {
public:
virtual bool RequestTaskDependencies(
const TaskID &task_id,
const std::vector<rpc::ObjectReference> &required_objects) = 0;
virtual bool IsTaskReady(const TaskID &task_id) const = 0;
virtual void RemoveTaskDependencies(const TaskID &task_id) = 0;
virtual ~TaskDependencyManagerInterface(){};
};
/// \class DependencyManager
///
/// Responsible for managing object dependencies for local workers calling
/// `ray.get` or `ray.wait` and arguments of queued tasks. The caller can
/// request object dependencies for a task or worker. The task manager will
/// determine which object dependencies are remote and will request that these
/// objects be made available locally, either via the object manager or by
/// storing an error if the object is lost.
class DependencyManager : public TaskDependencyManagerInterface {
public:
/// Create a task dependency manager.
DependencyManager(ObjectManagerInterface &object_manager,
ReconstructionPolicyInterface &reconstruction_policy)
: object_manager_(object_manager), reconstruction_policy_(reconstruction_policy) {}
/// Check whether an object is locally available.
///
/// \param object_id The object to check for.
/// \return Whether the object is local.
bool CheckObjectLocal(const ObjectID &object_id) const;
/// Get the address of the owner of this object. An address will only be
/// returned if the caller previously specified that this object is required
/// on this node, through a call to SubscribeGetDependencies or
/// SubscribeWaitDependencies.
///
/// \param[in] object_id The object whose owner to get.
/// \param[out] owner_address The address of the object's owner, if
/// available.
/// \return True if we have owner information for the object.
bool GetOwnerAddress(const ObjectID &object_id, rpc::Address *owner_address) const;
/// Start or update a worker's `ray.wait` request. This will attempt to make
/// any remote objects local, including previously requested objects. The
/// `ray.wait` request will stay active until the objects are made local or
/// the request for this worker is canceled, whichever occurs first.
///
/// This method may be called multiple times per worker on the same objects.
///
/// \param worker_id The ID of the worker that called `ray.wait`.
/// \param required_objects The objects required by the worker.
/// \return Void.
void StartOrUpdateWaitRequest(
const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects);
/// Cancel a worker's `ray.wait` request. We will no longer attempt to fetch
/// any objects that this worker requested previously, if no other task or
/// worker requires them.
///
/// \param worker_id The ID of the worker whose `ray.wait` request we should
/// cancel.
/// \return Void.
void CancelWaitRequest(const WorkerID &worker_id);
/// Start or update a worker's `ray.get` request. This will attempt to make
/// any remote objects local, including previously requested objects. The
/// `ray.get` request will stay active until the request for this worker is
/// canceled.
///
/// This method may be called multiple times per worker on the same objects.
///
/// \param worker_id The ID of the worker that called `ray.wait`.
/// \param required_objects The objects required by the worker.
/// \return Void.
void StartOrUpdateGetRequest(const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects);
/// Cancel a worker's `ray.get` request. We will no longer attempt to fetch
/// any objects that this worker requested previously, if no other task or
/// worker requires them.
///
/// \param worker_id The ID of the worker whose `ray.get` request we should
/// cancel.
/// \return Void.
void CancelGetRequest(const WorkerID &worker_id);
/// Request dependencies for a queued task. This will attempt to make any
/// remote objects local until the caller cancels the task's dependencies.
///
/// This method can only be called once per task, until the task has been
/// canceled.
///
/// \param task_id The task that requires the objects.
/// \param required_objects The objects required by the task.
/// \return Void.
bool RequestTaskDependencies(const TaskID &task_id,
const std::vector<rpc::ObjectReference> &required_objects);
/// Check whether a task is ready to run. The task ID must have been
/// previously added by the caller.
///
/// \param task_id The ID of the task to check.
/// \return Whether all of the dependencies for the task are
/// local.
bool IsTaskReady(const TaskID &task_id) const;
/// Cancel a task's dependencies. We will no longer attempt to fetch any
/// remote dependencies, if no other task or worker requires them.
///
/// This method can only be called on a task whose dependencies were added.
///
/// \param task_id The task that requires the objects.
/// \param required_objects The objects required by the task.
/// \return Void.
void RemoveTaskDependencies(const TaskID &task_id);
/// Handle an object becoming locally available.
///
/// \param object_id The object ID of the object to mark as locally
/// available.
/// \return A list of task IDs. This contains all added tasks that now have
/// all of their dependencies fulfilled.
std::vector<TaskID> HandleObjectLocal(const ray::ObjectID &object_id);
/// Handle an object that is no longer locally available.
///
/// \param object_id The object ID of the object that was previously locally
/// available.
/// \return A list of task IDs. This contains all added tasks that previously
/// had all of their dependencies fulfilled, but are now missing this object
/// dependency.
std::vector<TaskID> HandleObjectMissing(const ray::ObjectID &object_id);
/// Returns debug string for class.
///
/// \return string.
std::string DebugString() const;
private:
/// Metadata for an object that is needed by at least one executing worker
/// and/or one queued task.
struct ObjectDependencies {
ObjectDependencies(const rpc::ObjectReference &ref)
: owner_address(ref.owner_address()) {}
/// The tasks that depend on this object, either because the object is a task argument
/// or because the task called `ray.get` on the object.
std::unordered_set<TaskID> dependent_tasks;
/// The workers that depend on this object because they called `ray.get` on the
/// object.
std::unordered_set<WorkerID> dependent_get_requests;
/// The workers that depend on this object because they called `ray.wait` on the
/// object.
std::unordered_set<WorkerID> dependent_wait_requests;
/// If this object is required by at least one worker that called `ray.wait`, this is
/// the pull request ID.
uint64_t wait_request_id = 0;
/// The address of the worker that owns this object.
rpc::Address owner_address;
bool Empty() const {
return dependent_tasks.empty() && dependent_get_requests.empty() &&
dependent_wait_requests.empty();
}
};
/// A struct to represent the object dependencies of a task.
struct TaskDependencies {
TaskDependencies(const std::vector<rpc::ObjectReference> &deps)
: num_missing_dependencies(deps.size()) {
const auto dep_ids = ObjectRefsToIds(deps);
dependencies.insert(dep_ids.begin(), dep_ids.end());
}
/// The objects that the task depends on. These are the arguments to the
/// task. These must all be simultaneously local before the task is ready
/// to execute. Objects are removed from this set once
/// UnsubscribeGetDependencies is called.
absl::flat_hash_set<ObjectID> dependencies;
/// The number of object arguments that are not available locally. This
/// must be zero before the task is ready to execute.
size_t num_missing_dependencies;
/// Used to identify the pull request for the dependencies to the object
/// manager.
uint64_t pull_request_id = 0;
};
/// Stop tracking this object, if it is no longer needed by any worker or
/// queued task.
void RemoveObjectIfNotNeeded(
absl::flat_hash_map<ObjectID, ObjectDependencies>::iterator required_object_it);
/// Start tracking an object that is needed by a worker and/or queued task.
absl::flat_hash_map<ObjectID, ObjectDependencies>::iterator GetOrInsertRequiredObject(
const ObjectID &object_id, const rpc::ObjectReference &ref);
/// The object manager, used to fetch required objects from remote nodes.
ObjectManagerInterface &object_manager_;
/// The reconstruction policy, used to reconstruct required objects that no
/// longer exist on any live nodes.
/// TODO(swang): This class is no longer needed for reconstruction, since the
/// object's owner handles reconstruction. We use this class as a timer to
/// detect the owner's failure. Remove this class and move the timer logic
/// into this class.
ReconstructionPolicyInterface &reconstruction_policy_;
/// A map from the ID of a queued task to metadata about whether the task's
/// dependencies are all local or not.
absl::flat_hash_map<TaskID, TaskDependencies> queued_task_requests_;
/// A map from worker ID to the set of objects that the worker called
/// `ray.get` on and a pull request ID for these objects. The pull request ID
/// should be used to cancel the pull request in the object manager once the
/// worker cancels the `ray.get` request.
absl::flat_hash_map<WorkerID, std::pair<absl::flat_hash_set<ObjectID>, uint64_t>>
get_requests_;
/// A map from worker ID to the set of objects that the worker called
/// `ray.wait` on. Objects are removed from the set once they are made local,
/// or the worker cancels the `ray.wait` request.
absl::flat_hash_map<WorkerID, absl::flat_hash_set<ObjectID>> wait_requests_;
/// Deduplicated pool of objects required by all queued tasks and workers.
/// Objects are removed from this set once there are no more tasks or workers
/// that require it.
absl::flat_hash_map<ObjectID, ObjectDependencies> required_objects_;
/// The set of locally available objects. This is used to determine which
/// tasks are ready to run and which `ray.wait` requests can be finished.
std::unordered_set<ray::ObjectID> local_objects_;
friend class DependencyManagerTest;
};
} // namespace raylet
} // namespace ray
+372
View File
@@ -0,0 +1,372 @@
// Copyright 2017 The Ray 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 "ray/raylet/dependency_manager.h"
#include <list>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ray/common/task/task_util.h"
#include "ray/common/test_util.h"
namespace ray {
namespace raylet {
using ::testing::_;
using ::testing::InSequence;
using ::testing::Return;
class MockObjectManager : public ObjectManagerInterface {
public:
uint64_t Pull(const std::vector<rpc::ObjectReference> &object_refs) {
active_requests.insert(req_id);
return req_id++;
}
void CancelPull(uint64_t request_id) { ASSERT_TRUE(active_requests.erase(request_id)); }
uint64_t req_id = 1;
std::unordered_set<uint64_t> active_requests;
};
class MockReconstructionPolicy : public ReconstructionPolicyInterface {
public:
MOCK_METHOD2(ListenAndMaybeReconstruct,
void(const ObjectID &object_id, const rpc::Address &owner_address));
MOCK_METHOD1(Cancel, void(const ObjectID &object_id));
};
class DependencyManagerTest : public ::testing::Test {
public:
DependencyManagerTest()
: object_manager_mock_(),
reconstruction_policy_mock_(),
dependency_manager_(object_manager_mock_, reconstruction_policy_mock_) {}
void AssertNoLeaks() {
ASSERT_TRUE(dependency_manager_.required_objects_.empty());
ASSERT_TRUE(dependency_manager_.queued_task_requests_.empty());
ASSERT_TRUE(dependency_manager_.get_requests_.empty());
ASSERT_TRUE(dependency_manager_.wait_requests_.empty());
// All pull requests are canceled.
ASSERT_TRUE(object_manager_mock_.active_requests.empty());
}
MockObjectManager object_manager_mock_;
MockReconstructionPolicy reconstruction_policy_mock_;
DependencyManager dependency_manager_;
};
/// Test requesting the dependencies for a task. The dependency manager should
/// return the task ID as ready once all of its arguments are local.
TEST_F(DependencyManagerTest, TestSimpleTask) {
// Create a task with 3 arguments.
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
arguments.push_back(ObjectID::FromRandom());
}
TaskID task_id = RandomTaskId();
// No objects have been registered in the task dependency manager, so all
// arguments should be remote.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
bool ready =
dependency_manager_.RequestTaskDependencies(task_id, ObjectIdsToRefs(arguments));
ASSERT_FALSE(ready);
ASSERT_EQ(object_manager_mock_.active_requests.size(), 1);
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
// For each argument, tell the task dependency manager that the argument is
// local. All arguments should be canceled as they become available locally.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
auto ready_task_ids = dependency_manager_.HandleObjectLocal(arguments[0]);
ASSERT_TRUE(ready_task_ids.empty());
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
ready_task_ids = dependency_manager_.HandleObjectLocal(arguments[1]);
ASSERT_TRUE(ready_task_ids.empty());
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
// The task is ready to run.
ready_task_ids = dependency_manager_.HandleObjectLocal(arguments[2]);
ASSERT_EQ(ready_task_ids.size(), 1);
ASSERT_EQ(ready_task_ids.front(), task_id);
ASSERT_TRUE(dependency_manager_.IsTaskReady(task_id));
// Remove the task.
dependency_manager_.RemoveTaskDependencies(task_id);
AssertNoLeaks();
}
/// Test multiple tasks that depend on the same object. The dependency manager
/// should return all task IDs as ready once the object is local.
TEST_F(DependencyManagerTest, TestMultipleTasks) {
// Create 3 tasks that are dependent on the same object.
ObjectID argument_id = ObjectID::FromRandom();
std::vector<TaskID> dependent_tasks;
int num_dependent_tasks = 3;
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
for (int i = 0; i < num_dependent_tasks; i++) {
TaskID task_id = RandomTaskId();
dependent_tasks.push_back(task_id);
bool ready = dependency_manager_.RequestTaskDependencies(
task_id, ObjectIdsToRefs({argument_id}));
ASSERT_FALSE(ready);
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
// The object should be requested from the object manager once for each task.
ASSERT_EQ(object_manager_mock_.active_requests.size(), i + 1);
}
// Tell the task dependency manager that the object is local.
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
auto ready_task_ids = dependency_manager_.HandleObjectLocal(argument_id);
// Check that all tasks are now ready to run.
std::unordered_set<TaskID> added_tasks(dependent_tasks.begin(), dependent_tasks.end());
for (auto &id : ready_task_ids) {
ASSERT_TRUE(added_tasks.erase(id));
ASSERT_TRUE(dependency_manager_.IsTaskReady(id));
}
ASSERT_TRUE(added_tasks.empty());
for (auto &id : dependent_tasks) {
dependency_manager_.RemoveTaskDependencies(id);
}
AssertNoLeaks();
}
/// Test task with multiple dependencies. The dependency manager should return
/// the task ID as ready once all dependencies are local. If a dependency is
/// later evicted, the dependency manager should return the task ID as waiting.
TEST_F(DependencyManagerTest, TestTaskArgEviction) {
// Add a task with 3 arguments.
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
arguments.push_back(ObjectID::FromRandom());
}
TaskID task_id = RandomTaskId();
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
bool ready =
dependency_manager_.RequestTaskDependencies(task_id, ObjectIdsToRefs(arguments));
ASSERT_FALSE(ready);
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
// Tell the task dependency manager that each of the arguments is now
// available.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> ready_tasks;
ready_tasks = dependency_manager_.HandleObjectLocal(arguments[i]);
if (i == arguments.size() - 1) {
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), task_id);
} else {
ASSERT_TRUE(ready_tasks.empty());
}
}
ASSERT_TRUE(dependency_manager_.IsTaskReady(task_id));
// Simulate each of the arguments getting evicted. Each object should now be
// considered remote.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> waiting_tasks;
waiting_tasks = dependency_manager_.HandleObjectMissing(arguments[i]);
if (i == 0) {
// The first eviction should cause the task to go back to the waiting
// state.
ASSERT_EQ(waiting_tasks.size(), 1);
ASSERT_EQ(waiting_tasks.front(), task_id);
} else {
// The subsequent evictions shouldn't cause any more tasks to go back to
// the waiting state.
ASSERT_TRUE(waiting_tasks.empty());
}
ASSERT_FALSE(dependency_manager_.IsTaskReady(task_id));
}
// Tell the task dependency manager that each of the arguments is available
// again.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> ready_tasks;
ready_tasks = dependency_manager_.HandleObjectLocal(arguments[i]);
if (i == arguments.size() - 1) {
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), task_id);
} else {
ASSERT_TRUE(ready_tasks.empty());
}
}
ASSERT_TRUE(dependency_manager_.IsTaskReady(task_id));
dependency_manager_.RemoveTaskDependencies(task_id);
AssertNoLeaks();
}
/// Test `ray.get`. Worker calls ray.get on {oid1}, then {oid1, oid2}, then
/// {oid1, oid2, oid3}.
TEST_F(DependencyManagerTest, TestGet) {
WorkerID worker_id = WorkerID::FromRandom();
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
// Add the new argument to the list of dependencies to subscribe to.
ObjectID argument_id = ObjectID::FromRandom();
arguments.push_back(argument_id);
// Subscribe to the task's dependencies. All arguments except the last are
// duplicates of previous subscription calls. Each argument should only be
// requested from the node manager once.
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
auto prev_pull_reqs = object_manager_mock_.active_requests;
dependency_manager_.StartOrUpdateGetRequest(worker_id, ObjectIdsToRefs(arguments));
// Previous pull request for this worker should be canceled upon each new
// bundle.
ASSERT_EQ(object_manager_mock_.active_requests.size(), 1);
ASSERT_NE(object_manager_mock_.active_requests, prev_pull_reqs);
}
// Nothing happens if the same bundle is requested.
auto prev_pull_reqs = object_manager_mock_.active_requests;
dependency_manager_.StartOrUpdateGetRequest(worker_id, ObjectIdsToRefs(arguments));
ASSERT_EQ(object_manager_mock_.active_requests, prev_pull_reqs);
// All arguments should be canceled as they become available locally.
for (const auto &argument_id : arguments) {
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
// Cancel the pull request once the worker cancels the `ray.get`.
dependency_manager_.CancelGetRequest(worker_id);
AssertNoLeaks();
}
/// Test that when one of the objects becomes local after a `ray.wait` call,
/// all requests to remote nodes associated with the object are canceled.
TEST_F(DependencyManagerTest, TestWait) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> oids;
for (int i = 0; i < num_objects; i++) {
oids.push_back(ObjectID::FromRandom());
}
// Simulate a worker calling `ray.wait` on some objects.
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _))
.Times(num_objects);
dependency_manager_.StartOrUpdateWaitRequest(worker_id, ObjectIdsToRefs(oids));
ASSERT_EQ(object_manager_mock_.active_requests.size(), num_objects);
for (int i = 0; i < num_objects; i++) {
// Object is local.
EXPECT_CALL(reconstruction_policy_mock_, Cancel(oids[i]));
auto ready_task_ids = dependency_manager_.HandleObjectLocal(oids[i]);
// Local object gets evicted. The `ray.wait` call should not be
// reactivated.
auto waiting_task_ids = dependency_manager_.HandleObjectMissing(oids[i]);
ASSERT_TRUE(waiting_task_ids.empty());
ASSERT_EQ(object_manager_mock_.active_requests.size(), num_objects - i - 1);
}
AssertNoLeaks();
}
/// Test that when no objects are locally available, a `ray.wait` call makes
/// the correct requests to remote nodes and correctly cancels the requests
/// when the `ray.wait` call is canceled.
TEST_F(DependencyManagerTest, TestWaitThenCancel) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> oids;
for (int i = 0; i < num_objects; i++) {
oids.push_back(ObjectID::FromRandom());
}
// Simulate a worker calling `ray.wait` on some objects.
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _))
.Times(num_objects);
dependency_manager_.StartOrUpdateWaitRequest(worker_id, ObjectIdsToRefs(oids));
ASSERT_EQ(object_manager_mock_.active_requests.size(), num_objects);
auto prev_pull_reqs = object_manager_mock_.active_requests;
// Check that it's okay to call `ray.wait` on the same objects again. No new
// calls should be made to try and make the objects local.
dependency_manager_.StartOrUpdateWaitRequest(worker_id, ObjectIdsToRefs(oids));
ASSERT_EQ(object_manager_mock_.active_requests, prev_pull_reqs);
// Cancel the worker's `ray.wait`.
EXPECT_CALL(reconstruction_policy_mock_, Cancel(_)).Times(num_objects);
dependency_manager_.CancelWaitRequest(worker_id);
AssertNoLeaks();
}
/// Test that when one of the objects is already local at the time of the
/// `ray.wait` call, the `ray.wait` call does not trigger any requests to
/// remote nodes for that object.
TEST_F(DependencyManagerTest, TestWaitObjectLocal) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> oids;
for (int i = 0; i < num_objects; i++) {
oids.push_back(ObjectID::FromRandom());
}
// Simulate one of the objects becoming local. The later `ray.wait` call
// should have no effect because the object is already local.
const ObjectID local_object_id = std::move(oids.back());
auto ready_task_ids = dependency_manager_.HandleObjectLocal(local_object_id);
ASSERT_TRUE(ready_task_ids.empty());
// Simulate a worker calling `ray.wait` on the objects. It should only make
// requests for the objects that are not local.
for (const auto &object_id : oids) {
if (object_id != local_object_id) {
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(object_id, _));
}
}
dependency_manager_.StartOrUpdateWaitRequest(worker_id, ObjectIdsToRefs(oids));
ASSERT_EQ(object_manager_mock_.active_requests.size(), num_objects - 1);
// Simulate the local object getting evicted. The `ray.wait` call should not
// be reactivated.
auto waiting_task_ids = dependency_manager_.HandleObjectMissing(local_object_id);
ASSERT_TRUE(waiting_task_ids.empty());
ASSERT_EQ(object_manager_mock_.active_requests.size(), num_objects - 1);
// Cancel the worker's `ray.wait`.
for (const auto &object_id : oids) {
if (object_id != local_object_id) {
EXPECT_CALL(reconstruction_policy_mock_, Cancel(object_id));
}
}
dependency_manager_.CancelWaitRequest(worker_id);
AssertNoLeaks();
}
} // namespace raylet
} // namespace ray
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+35 -100
View File
@@ -158,7 +158,7 @@ NodeManager::NodeManager(boost::asio::io_service &io_service, const NodeID &self
},
RayConfig::instance().object_timeout_milliseconds(), self_node_id_, gcs_client_,
object_directory_),
task_dependency_manager_(object_manager, reconstruction_policy_),
dependency_manager_(object_manager, reconstruction_policy_),
node_manager_server_("NodeManager", config.node_manager_port),
node_manager_service_(io_service, *this),
agent_manager_service_handler_(
@@ -218,7 +218,7 @@ NodeManager::NodeManager(boost::asio::io_service &io_service, const NodeID &self
PublishInfeasibleTaskError(task);
};
cluster_task_manager_ = std::shared_ptr<ClusterTaskManager>(new ClusterTaskManager(
self_node_id_, new_resource_scheduler_, task_dependency_manager_, is_owner_alive,
self_node_id_, new_resource_scheduler_, dependency_manager_, is_owner_alive,
get_node_info_func, announce_infeasible_task));
placement_group_resource_manager_ =
std::make_shared<NewPlacementGroupResourceManager>(new_resource_scheduler_);
@@ -382,7 +382,7 @@ void NodeManager::HandleJobFinished(const JobID &job_id, const JobTableData &job
for (const auto &worker : workers) {
if (!worker->IsDetachedActor()) {
// Clean up any open ray.wait calls that the worker made.
task_dependency_manager_.UnsubscribeWaitDependencies(worker->WorkerId());
dependency_manager_.CancelWaitRequest(worker->WorkerId());
// Mark the worker as dead so further messages from it are ignored
// (except DisconnectClient).
worker->MarkDead();
@@ -390,18 +390,6 @@ void NodeManager::HandleJobFinished(const JobID &job_id, const JobTableData &job
KillWorker(worker);
}
}
if (!new_scheduler_enabled_) {
// Remove all tasks for this job from the scheduling queues, mark
// the results for these tasks as not required, cancel any attempts
// at reconstruction. Note that at this time the workers are likely
// alive because of the delay in killing workers.
auto tasks_to_remove = local_queues_.GetTaskIdsForJob(job_id);
task_dependency_manager_.RemoveTasksAndRelatedObjects(tasks_to_remove);
// NOTE(swang): SchedulingQueue::RemoveTasks modifies its argument so we must
// call it last.
local_queues_.RemoveTasks(tasks_to_remove);
}
}
void NodeManager::Heartbeat() {
@@ -1034,7 +1022,7 @@ void NodeManager::ResourceUsageAdded(const NodeID &node_id,
if (state != TaskState::INFEASIBLE) {
// Don't unsubscribe for infeasible tasks because we never subscribed in
// the first place.
RAY_CHECK(task_dependency_manager_.UnsubscribeGetDependencies(task_id));
dependency_manager_.RemoveTaskDependencies(task_id);
}
// Attempt to forward the task. If this fails to forward the task,
// the task will be resubmit locally.
@@ -1403,7 +1391,7 @@ void NodeManager::ProcessDisconnectClientMessage(
AsyncResolveObjectsFinish(client, task_id, true);
}
// Clean up any open ray.wait calls that the worker made.
task_dependency_manager_.UnsubscribeWaitDependencies(worker->WorkerId());
dependency_manager_.CancelWaitRequest(worker->WorkerId());
}
// Erase any lease metadata.
@@ -1425,9 +1413,7 @@ void NodeManager::ProcessDisconnectClientMessage(
// If the worker was an actor, it'll be cleaned by GCS.
if (actor_id.IsNil()) {
Task task;
if (local_queues_.RemoveTask(task_id, &task)) {
TreatTaskAsFailed(task, ErrorType::WORKER_DIED);
}
static_cast<void>(local_queues_.RemoveTask(task_id, &task));
}
if (!intentional_disconnect) {
@@ -1501,14 +1487,14 @@ void NodeManager::ProcessFetchOrReconstructMessage(
const auto refs =
FlatbufferToObjectReference(*message->object_ids(), *message->owner_addresses());
if (message->fetch_only()) {
for (const auto &ref : refs) {
ObjectID object_id = ObjectID::FromBinary(ref.object_id());
// If only a fetch is required, then do not subscribe to the
// dependencies to the task dependency manager.
if (!task_dependency_manager_.CheckObjectLocal(object_id)) {
// Fetch the object if it's not already local.
RAY_CHECK_OK(object_manager_.Pull(object_id, ref.owner_address()));
}
std::shared_ptr<WorkerInterface> worker = worker_pool_.GetRegisteredWorker(client);
if (!worker) {
worker = worker_pool_.GetRegisteredDriver(client);
}
if (worker) {
// This will start a fetch for the objects that gets canceled once the
// objects are local, or if the worker dies.
dependency_manager_.StartOrUpdateWaitRequest(worker->WorkerId(), refs);
}
} else {
// The values are needed. Add all requested objects to the list to
@@ -1544,7 +1530,7 @@ void NodeManager::ProcessWaitRequestMessage(
bool resolve_objects = false;
for (auto const &object_id : object_ids) {
if (!task_dependency_manager_.CheckObjectLocal(object_id)) {
if (!dependency_manager_.CheckObjectLocal(object_id)) {
// At least one object requires resolution.
resolve_objects = true;
}
@@ -1904,11 +1890,7 @@ void NodeManager::HandleCancelWorkerLease(const rpc::CancelWorkerLeaseRequest &r
bool canceled;
if (new_scheduler_enabled_) {
canceled = cluster_task_manager_->CancelTask(task_id);
if (canceled) {
// We have not yet granted the worker lease. Cancel it now.
task_dependency_manager_.TaskCanceled(task_id);
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
} else {
if (!canceled) {
// There are 2 cases here.
// 1. We haven't received the lease request yet. It's the caller's job to
// retry the cancellation once we've received the request.
@@ -1925,8 +1907,9 @@ void NodeManager::HandleCancelWorkerLease(const rpc::CancelWorkerLeaseRequest &r
if (removed_task.OnDispatch()) {
// We have not yet granted the worker lease. Cancel it now.
removed_task.OnCancellation()();
task_dependency_manager_.TaskCanceled(task_id);
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
if (removed_task_state == TaskState::WAITING) {
dependency_manager_.RemoveTaskDependencies(task_id);
}
} else {
// We already granted the worker lease and sent the reply. Re-queue the
// task and wait for the requester to return the leased worker.
@@ -2035,7 +2018,6 @@ void NodeManager::ScheduleTasks(
// submission vs. registering remaining queued placeable tasks here.
std::unordered_set<TaskID> move_task_set;
for (const auto &task : local_queues_.GetTasks(TaskState::PLACEABLE)) {
task_dependency_manager_.TaskPending(task);
move_task_set.insert(task.GetTaskSpecification().TaskId());
PublishInfeasibleTaskError(task);
// Assert that this placeable task is not feasible locally (necessary but not
@@ -2051,37 +2033,6 @@ void NodeManager::ScheduleTasks(
RAY_CHECK(local_queues_.GetTasks(TaskState::PLACEABLE).size() == 0);
}
void NodeManager::TreatTaskAsFailed(const Task &task, const ErrorType &error_type) {
const TaskSpecification &spec = task.GetTaskSpecification();
RAY_LOG(DEBUG) << "Treating task " << spec.TaskId() << " as failed because of error "
<< ErrorType_Name(error_type) << ".";
// Loop over the return IDs (except the dummy ID) and store a fake object in
// the object store.
int64_t num_returns = spec.NumReturns();
if (spec.IsActorCreationTask()) {
// TODO(rkn): We subtract 1 to avoid the dummy ID. However, this leaks
// information about the TaskSpecification implementation.
num_returns -= 1;
}
// Determine which IDs should be marked as failed.
std::vector<rpc::ObjectReference> objects_to_fail;
for (int64_t i = 0; i < num_returns; i++) {
rpc::ObjectReference ref;
ref.set_object_id(spec.ReturnId(i).Binary());
ref.mutable_owner_address()->CopyFrom(spec.CallerAddress());
objects_to_fail.push_back(ref);
}
const JobID job_id = task.GetTaskSpecification().JobId();
MarkObjectsAsFailed(error_type, objects_to_fail, job_id);
task_dependency_manager_.TaskCanceled(spec.TaskId());
// Notify the task dependency manager that we no longer need this task's
// object dependencies. TODO(swang): Ideally, we would check the return value
// here. However, we don't know at this point if the task was in the WAITING
// or READY queue before, in which case we would not have been subscribed to
// its dependencies.
task_dependency_manager_.UnsubscribeGetDependencies(spec.TaskId());
}
void NodeManager::MarkObjectsAsFailed(
const ErrorType &error_type, const std::vector<rpc::ObjectReference> objects_to_fail,
const JobID &job_id) {
@@ -2189,7 +2140,7 @@ void NodeManager::HandleDirectCallTaskUnblocked(
// First, always release task dependencies. This ensures we don't leak resources even
// if we don't need to unblock the worker below.
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
dependency_manager_.CancelGetRequest(worker->WorkerId());
if (new_scheduler_enabled_) {
// Important: avoid double unblocking if the unblock RPC finishes after task end.
@@ -2281,15 +2232,10 @@ void NodeManager::AsyncResolveObjects(
// fetched and/or restarted as necessary, until the objects become local
// or are unsubscribed.
if (ray_get) {
// TODO(ekl) using the assigned task id is a hack to handle unsubscription for
// HandleDirectCallUnblocked.
auto &task_id = mark_worker_blocked ? current_task_id : worker->GetAssignedTaskId();
if (!task_id.IsNil()) {
task_dependency_manager_.SubscribeGetDependencies(task_id, required_object_refs);
}
dependency_manager_.StartOrUpdateGetRequest(worker->WorkerId(), required_object_refs);
} else {
task_dependency_manager_.SubscribeWaitDependencies(worker->WorkerId(),
required_object_refs);
dependency_manager_.StartOrUpdateWaitRequest(worker->WorkerId(),
required_object_refs);
}
}
@@ -2341,13 +2287,13 @@ void NodeManager::AsyncResolveObjectsFinish(
worker = worker_pool_.GetRegisteredDriver(client);
}
RAY_CHECK(worker);
// Unsubscribe from any `ray.get` objects that the task was blocked on. Any
// fetch or reconstruction operations to make the objects local are canceled.
// `ray.wait` calls will stay active until the objects become local, or the
// task/actor that called `ray.wait` exits.
task_dependency_manager_.UnsubscribeGetDependencies(current_task_id);
dependency_manager_.CancelGetRequest(worker->WorkerId());
// Mark the task as unblocked.
RAY_CHECK(worker);
if (was_blocked) {
worker->RemoveBlockedTaskId(current_task_id);
local_queues_.RemoveBlockedTaskId(current_task_id);
@@ -2358,7 +2304,7 @@ void NodeManager::EnqueuePlaceableTask(const Task &task) {
// TODO(atumanov): add task lookup hashmap and change EnqueuePlaceableTask to take
// a vector of TaskIDs. Trigger MoveTask internally.
// Subscribe to the task's dependencies.
bool args_ready = task_dependency_manager_.SubscribeGetDependencies(
bool args_ready = dependency_manager_.RequestTaskDependencies(
task.GetTaskSpecification().TaskId(), task.GetDependencies());
// Enqueue the task. If all dependencies are available, then the task is queued
// in the READY state, else the WAITING state.
@@ -2369,10 +2315,6 @@ void NodeManager::EnqueuePlaceableTask(const Task &task) {
} else {
local_queues_.QueueTasks({task}, TaskState::WAITING);
}
// Mark the task as pending. Once the task has finished execution, or once it
// has been forwarded to another node, the task must be marked as canceled in
// the TaskDependencyManager.
task_dependency_manager_.TaskPending(task);
}
void NodeManager::AssignTask(const std::shared_ptr<WorkerInterface> &worker,
@@ -2470,12 +2412,11 @@ bool NodeManager::FinishAssignedTask(const std::shared_ptr<WorkerInterface> &wor
} else {
// If this was a non-actor task, then cancel any ray.wait calls that were
// made during the task execution.
task_dependency_manager_.UnsubscribeWaitDependencies(worker.WorkerId());
dependency_manager_.CancelWaitRequest(worker.WorkerId());
}
// Notify the task dependency manager that this task has finished execution.
task_dependency_manager_.UnsubscribeGetDependencies(spec.TaskId());
task_dependency_manager_.TaskCanceled(task_id);
dependency_manager_.CancelGetRequest(worker.WorkerId());
if (!spec.IsActorCreationTask()) {
// Unset the worker's assigned task. We keep the assigned task ID for
@@ -2507,8 +2448,7 @@ void NodeManager::HandleTaskReconstruction(const TaskID &task_id,
const ObjectID &required_object_id) {
// Get the owner's address.
rpc::Address owner_addr;
bool has_owner =
task_dependency_manager_.GetOwnerAddress(required_object_id, &owner_addr);
bool has_owner = dependency_manager_.GetOwnerAddress(required_object_id, &owner_addr);
if (has_owner) {
if (!RayConfig::instance().object_pinning_enabled()) {
// LRU eviction is enabled. The object may still be in scope, but we
@@ -2573,7 +2513,7 @@ void NodeManager::HandleTaskReconstruction(const TaskID &task_id,
void NodeManager::HandleObjectLocal(const ObjectID &object_id) {
// Notify the task dependency manager that this object is local.
const auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(object_id);
const auto ready_task_ids = dependency_manager_.HandleObjectLocal(object_id);
RAY_LOG(DEBUG) << "Object local " << object_id << ", "
<< " on " << self_node_id_ << ", " << ready_task_ids.size()
<< " tasks ready";
@@ -2621,7 +2561,7 @@ bool NodeManager::IsActorCreationTask(const TaskID &task_id) {
void NodeManager::HandleObjectMissing(const ObjectID &object_id) {
// Notify the task dependency manager that this object is no longer local.
const auto waiting_task_ids = task_dependency_manager_.HandleObjectMissing(object_id);
const auto waiting_task_ids = dependency_manager_.HandleObjectMissing(object_id);
std::stringstream result;
result << "Object missing " << object_id << ", "
<< " on " << self_node_id_ << ", " << waiting_task_ids.size()
@@ -2689,10 +2629,6 @@ void NodeManager::ForwardTaskOrResubmit(const Task &task, const NodeID &node_man
RAY_LOG(INFO) << "Failed to forward task " << task_id
<< " to node manager " << node_manager_id;
// Mark the failed task as pending to let other raylets know that we still
// have the task. TaskDependencyManager::TaskPending() is assumed to be
// idempotent.
task_dependency_manager_.TaskPending(task);
// The task is not for an actor and may therefore be placed on another
// node immediately. Send it to the scheduling policy to be placed again.
local_queues_.QueueTasks({task}, TaskState::PLACEABLE);
@@ -2743,7 +2679,7 @@ void NodeManager::FinishAssignTask(const std::shared_ptr<WorkerInterface> &worke
local_queues_.QueueTasks({assigned_task}, TaskState::RUNNING);
// Notify the task dependency manager that we no longer need this task's
// object dependencies.
RAY_CHECK(task_dependency_manager_.UnsubscribeGetDependencies(spec.TaskId()));
dependency_manager_.RemoveTaskDependencies(spec.TaskId());
} else {
RAY_LOG(WARNING) << "Failed to send task to worker, disconnecting client";
// We failed to send the task to the worker, so disconnect the worker.
@@ -2770,7 +2706,7 @@ void NodeManager::ProcessSubscribePlasmaReady(
auto message = flatbuffers::GetRoot<protocol::SubscribePlasmaReady>(message_data);
ObjectID id = from_flatbuf<ObjectID>(*message->object_id());
if (task_dependency_manager_.CheckObjectLocal(id)) {
if (dependency_manager_.CheckObjectLocal(id)) {
// Object is already local, so we directly fire the callback to tell the core worker
// that the plasma object is ready.
rpc::PlasmaObjectReadyRequest request;
@@ -2797,8 +2733,7 @@ void NodeManager::ProcessSubscribePlasmaReady(
// is local at this time but when the core worker was notified, the object is
// is evicted. The core worker should be able to handle evicted object in this
// case.
task_dependency_manager_.SubscribeWaitDependencies(associated_worker->WorkerId(),
refs);
dependency_manager_.StartOrUpdateWaitRequest(associated_worker->WorkerId(), refs);
// Add this worker to the listeners for the object ID.
{
@@ -2864,7 +2799,7 @@ std::string NodeManager::DebugString() const {
result << "\n" << worker_pool_.DebugString();
result << "\n" << local_queues_.DebugString();
result << "\n" << reconstruction_policy_.DebugString();
result << "\n" << task_dependency_manager_.DebugString();
result << "\n" << dependency_manager_.DebugString();
{
absl::MutexLock guard(&plasma_object_notification_lock_);
result << "\nnum async plasma notifications: "
+4 -15
View File
@@ -35,7 +35,7 @@
#include "ray/raylet/scheduling_policy.h"
#include "ray/raylet/scheduling_queue.h"
#include "ray/raylet/reconstruction_policy.h"
#include "ray/raylet/task_dependency_manager.h"
#include "ray/raylet/dependency_manager.h"
#include "ray/raylet/worker_pool.h"
#include "ray/rpc/worker/core_worker_client_pool.h"
#include "ray/util/ordered_set.h"
@@ -239,18 +239,6 @@ class NodeManager : public rpc::NodeManagerServiceHandler {
/// \param task The task in question.
/// \return Void.
void EnqueuePlaceableTask(const Task &task);
/// This will treat a task removed from the local queue as if it had been
/// executed and failed. This is done by looping over the task return IDs and
/// for each ID storing an object that represents a failure in the object
/// store. When clients retrieve these objects, they will raise
/// application-level exceptions. State for the task will be cleaned up as if
/// it were any other task that had been assigned, executed, and removed from
/// the local queue.
///
/// \param task The task to fail.
/// \param error_type The type of the error that caused this task to fail.
/// \return Void.
void TreatTaskAsFailed(const Task &task, const ErrorType &error_type);
/// Mark the specified objects as failed with the given error type.
///
/// \param error_type The type of the error that caused this task to fail.
@@ -707,8 +695,9 @@ class NodeManager : public rpc::NodeManagerServiceHandler {
SchedulingPolicy scheduling_policy_;
/// The reconstruction policy for deciding when to re-execute a task.
ReconstructionPolicy reconstruction_policy_;
/// A manager to make waiting tasks's missing object dependencies available.
TaskDependencyManager task_dependency_manager_;
/// A manager to resolve objects needed by queued tasks and workers that
/// called `ray.get` or `ray.wait`.
DependencyManager dependency_manager_;
std::unique_ptr<AgentManager> agent_manager_;
@@ -102,7 +102,7 @@ bool ClusterTaskManager::WaitForTaskArgsRequests(Work work) {
auto object_ids = task.GetTaskSpecification().GetDependencies();
bool can_dispatch = true;
if (object_ids.size() > 0) {
bool args_ready = task_dependency_manager_.SubscribeGetDependencies(
bool args_ready = task_dependency_manager_.RequestTaskDependencies(
task.GetTaskSpecification().TaskId(), task.GetDependencies());
if (args_ready) {
RAY_LOG(DEBUG) << "Args already ready, task can be dispatched "
@@ -164,7 +164,8 @@ void ClusterTaskManager::DispatchScheduledTasksToWorkers(
<< "'s caller is no longer running. Cancelling task.";
worker_pool.PushWorker(worker);
if (!spec.GetDependencies().empty()) {
RAY_CHECK(task_dependency_manager_.UnsubscribeGetDependencies(spec.TaskId()));
task_dependency_manager_.RemoveTaskDependencies(
task.GetTaskSpecification().TaskId());
}
work_it = dispatch_queue.erase(work_it);
} else {
@@ -179,7 +180,8 @@ void ClusterTaskManager::DispatchScheduledTasksToWorkers(
}
if (remove) {
if (!spec.GetDependencies().empty()) {
RAY_CHECK(task_dependency_manager_.UnsubscribeGetDependencies(spec.TaskId()));
task_dependency_manager_.RemoveTaskDependencies(
task.GetTaskSpecification().TaskId());
}
work_it = dispatch_queue.erase(work_it);
} else {
@@ -313,7 +315,8 @@ bool ClusterTaskManager::CancelTask(const TaskID &task_id) {
RemoveFromBacklogTracker(task);
ReplyCancelled(*work_it);
if (!task.GetTaskSpecification().GetDependencies().empty()) {
RAY_CHECK(task_dependency_manager_.UnsubscribeGetDependencies(task_id));
task_dependency_manager_.RemoveTaskDependencies(
task.GetTaskSpecification().TaskId());
}
work_queue.erase(work_it);
if (work_queue.empty()) {
@@ -347,9 +350,11 @@ bool ClusterTaskManager::CancelTask(const TaskID &task_id) {
RemoveFromBacklogTracker(task);
ReplyCancelled(iter->second);
if (!task.GetTaskSpecification().GetDependencies().empty()) {
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
task_dependency_manager_.RemoveTaskDependencies(task_id);
}
waiting_tasks_.erase(iter);
task_dependency_manager_.RemoveTaskDependencies(task_id);
return true;
}
@@ -4,8 +4,8 @@
#include "absl/container/flat_hash_set.h"
#include "ray/common/task/task.h"
#include "ray/common/task/task_common.h"
#include "ray/raylet/dependency_manager.h"
#include "ray/raylet/scheduling/cluster_resource_scheduler.h"
#include "ray/raylet/task_dependency_manager.h"
#include "ray/raylet/worker.h"
#include "ray/raylet/worker_pool.h"
#include "ray/rpc/grpc_client.h"
@@ -53,7 +53,7 @@ class ClusterTaskManager {
/// \param gcs_client: A gcs client.
ClusterTaskManager(const NodeID &self_node_id,
std::shared_ptr<ClusterResourceScheduler> cluster_resource_scheduler,
TaskDependencyManagerInterface &task_dependency_manager_,
TaskDependencyManagerInterface &task_dependency_manager,
std::function<bool(const WorkerID &, const NodeID &)> is_owner_alive,
NodeInfoGetter get_node_info,
std::function<void(const Task &)> announce_infeasible_task);
@@ -96,14 +96,14 @@ Task CreateTask(const std::unordered_map<std::string, double> &required_resource
class MockTaskDependencyManager : public TaskDependencyManagerInterface {
public:
bool SubscribeGetDependencies(
bool RequestTaskDependencies(
const TaskID &task_id, const std::vector<rpc::ObjectReference> &required_objects) {
RAY_CHECK(subscribed_tasks.insert(task_id).second);
return task_ready_;
}
bool UnsubscribeGetDependencies(const TaskID &task_id) {
return subscribed_tasks.erase(task_id);
void RemoveTaskDependencies(const TaskID &task_id) {
RAY_CHECK(subscribed_tasks.erase(task_id));
}
bool IsTaskReady(const TaskID &task_id) const { return task_ready_; }
-474
View File
@@ -1,474 +0,0 @@
// Copyright 2017 The Ray 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 "ray/raylet/task_dependency_manager.h"
#include "absl/time/clock.h"
#include "ray/stats/stats.h"
namespace ray {
namespace raylet {
TaskDependencyManager::TaskDependencyManager(
ObjectManagerInterface &object_manager,
ReconstructionPolicyInterface &reconstruction_policy)
: object_manager_(object_manager), reconstruction_policy_(reconstruction_policy) {}
bool TaskDependencyManager::CheckObjectLocal(const ObjectID &object_id) const {
return local_objects_.count(object_id) == 1;
}
bool TaskDependencyManager::CheckObjectRequired(const ObjectID &object_id,
rpc::Address *owner_address) const {
const TaskID task_id = object_id.TaskId();
auto task_entry = required_tasks_.find(task_id);
// If there are no subscribed tasks that are dependent on the object, then do
// nothing.
if (task_entry == required_tasks_.end()) {
return false;
}
if (task_entry->second.count(object_id) == 0) {
return false;
}
// If the object is already local, then the dependency is fulfilled. Do
// nothing.
if (local_objects_.count(object_id) == 1) {
return false;
}
// If the task that creates the object is pending execution, then the
// dependency will be fulfilled locally. Do nothing.
if (pending_tasks_.count(task_id) == 1) {
return false;
}
if (owner_address != nullptr) {
*owner_address = task_entry->second.at(object_id).owner_address;
}
return true;
}
void TaskDependencyManager::HandleRemoteDependencyRequired(const ObjectID &object_id) {
rpc::Address owner_address;
bool required = CheckObjectRequired(object_id, &owner_address);
// If the object is required, then try to make the object available locally.
if (required) {
auto inserted = required_objects_.insert(object_id);
if (inserted.second) {
// If we haven't already, request the object manager to pull it from a
// remote node.
RAY_CHECK_OK(object_manager_.Pull(object_id, owner_address));
reconstruction_policy_.ListenAndMaybeReconstruct(object_id, owner_address);
}
}
}
void TaskDependencyManager::HandleRemoteDependencyCanceled(const ObjectID &object_id) {
bool required = CheckObjectRequired(object_id, nullptr);
// If the object is no longer required, then cancel the object.
if (!required) {
auto it = required_objects_.find(object_id);
if (it != required_objects_.end()) {
object_manager_.CancelPull(object_id);
reconstruction_policy_.Cancel(object_id);
required_objects_.erase(it);
}
}
}
std::vector<TaskID> TaskDependencyManager::HandleObjectLocal(
const ray::ObjectID &object_id) {
// Add the object to the table of locally available objects.
auto inserted = local_objects_.insert(object_id);
RAY_CHECK(inserted.second) << object_id;
// Find all tasks and workers that depend on the newly available object.
std::vector<TaskID> ready_task_ids;
auto creating_task_entry = required_tasks_.find(object_id.TaskId());
if (creating_task_entry != required_tasks_.end()) {
auto object_entry = creating_task_entry->second.find(object_id);
if (object_entry != creating_task_entry->second.end()) {
// Loop through all tasks that depend on the newly available object.
for (const auto &dependent_task_id : object_entry->second.dependent_tasks) {
auto &task_entry = task_dependencies_[dependent_task_id];
task_entry.num_missing_get_dependencies--;
// If the dependent task now has all of its arguments ready, it's ready
// to run.
if (task_entry.num_missing_get_dependencies == 0) {
ready_task_ids.push_back(dependent_task_id);
}
}
// Remove the dependency from all workers that called `ray.wait` on the
// newly available object.
for (const auto &worker_id : object_entry->second.dependent_workers) {
RAY_CHECK(worker_dependencies_[worker_id].erase(object_id) > 0);
}
// Clear all workers that called `ray.wait` on this object, since the
// `ray.wait` calls can now return the object as ready.
object_entry->second.dependent_workers.clear();
// If there are no more tasks or workers dependent on the local object or
// the task that created it, then remove the entry completely.
if (object_entry->second.Empty()) {
creating_task_entry->second.erase(object_entry);
if (creating_task_entry->second.empty()) {
required_tasks_.erase(creating_task_entry);
}
}
}
}
// The object is now local, so cancel any in-progress operations to make the
// object local.
HandleRemoteDependencyCanceled(object_id);
return ready_task_ids;
}
std::vector<TaskID> TaskDependencyManager::HandleObjectMissing(
const ray::ObjectID &object_id) {
// Remove the object from the table of locally available objects.
auto erased = local_objects_.erase(object_id);
RAY_CHECK(erased == 1);
// Find any tasks that are dependent on the missing object.
std::vector<TaskID> waiting_task_ids;
TaskID creating_task_id = object_id.TaskId();
auto creating_task_entry = required_tasks_.find(creating_task_id);
if (creating_task_entry != required_tasks_.end()) {
auto object_entry = creating_task_entry->second.find(object_id);
if (object_entry != creating_task_entry->second.end()) {
for (auto &dependent_task_id : object_entry->second.dependent_tasks) {
auto &task_entry = task_dependencies_[dependent_task_id];
// If the dependent task had all of its arguments ready, it was ready to
// run but must be switched to waiting since one of its arguments is now
// missing.
if (task_entry.num_missing_get_dependencies == 0) {
waiting_task_ids.push_back(dependent_task_id);
// During normal execution we should be able to include the check
// RAY_CHECK(pending_tasks_.count(dependent_task_id) == 1);
// However, this invariant will not hold during unit test execution.
}
task_entry.num_missing_get_dependencies++;
}
}
}
// The object is no longer local. Try to make the object local if necessary.
HandleRemoteDependencyRequired(object_id);
// Process callbacks for all of the tasks dependent on the object that are
// now ready to run.
return waiting_task_ids;
}
bool TaskDependencyManager::SubscribeGetDependencies(
const TaskID &task_id, const std::vector<rpc::ObjectReference> &required_objects) {
auto &task_entry = task_dependencies_[task_id];
// Record the task's dependencies.
for (const auto &object : required_objects) {
const auto &object_id = ObjectID::FromBinary(object.object_id());
auto inserted = task_entry.get_dependencies.insert(object_id);
if (inserted.second) {
RAY_LOG(DEBUG) << "Task " << task_id << " blocked on object " << object_id;
// Get the ID of the task that creates the dependency.
TaskID creating_task_id = object_id.TaskId();
// Determine whether the dependency can be fulfilled by the local node.
if (local_objects_.count(object_id) == 0) {
// The object is not local.
task_entry.num_missing_get_dependencies++;
}
auto it = required_tasks_[creating_task_id].find(object_id);
if (it == required_tasks_[creating_task_id].end()) {
it = required_tasks_[creating_task_id]
.emplace(object_id, ObjectDependencies(object))
.first;
}
// Add the subscribed task to the mapping from object ID to list of
// dependent tasks.
it->second.dependent_tasks.insert(task_id);
}
}
// These dependencies are required by the given task. Try to make them local
// if necessary.
for (const auto &object : required_objects) {
const auto &object_id = ObjectID::FromBinary(object.object_id());
HandleRemoteDependencyRequired(object_id);
}
// Return whether all dependencies are local.
return (task_entry.num_missing_get_dependencies == 0);
}
bool TaskDependencyManager::IsTaskReady(const TaskID &task_id) const {
auto task_entry = task_dependencies_.find(task_id);
RAY_CHECK(task_entry != task_dependencies_.end());
return task_entry->second.num_missing_get_dependencies == 0;
}
void TaskDependencyManager::SubscribeWaitDependencies(
const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects) {
auto &worker_entry = worker_dependencies_[worker_id];
// Record the worker's dependencies.
for (const auto &object : required_objects) {
const auto &object_id = ObjectID::FromBinary(object.object_id());
if (local_objects_.count(object_id) == 0) {
RAY_LOG(DEBUG) << "Worker " << worker_id << " called ray.wait on remote object "
<< object_id;
// Only add the dependency if the object is not local. If the object is
// local, then the `ray.wait` call can already return it.
auto inserted = worker_entry.insert(object_id);
if (inserted.second) {
// Get the ID of the task that creates the dependency.
TaskID creating_task_id = object_id.TaskId();
auto it = required_tasks_[creating_task_id].find(object_id);
if (it == required_tasks_[creating_task_id].end()) {
it = required_tasks_[creating_task_id]
.emplace(object_id, ObjectDependencies(object))
.first;
}
// Add the subscribed worker to the mapping from object ID to list of
// dependent workers.
it->second.dependent_workers.insert(worker_id);
}
}
}
// These dependencies are required by the given worker. Try to make them
// local if necessary.
for (const auto &object : required_objects) {
const auto &object_id = ObjectID::FromBinary(object.object_id());
HandleRemoteDependencyRequired(object_id);
}
}
bool TaskDependencyManager::UnsubscribeGetDependencies(const TaskID &task_id) {
RAY_LOG(DEBUG) << "Task " << task_id << " no longer blocked";
// Remove the task from the table of subscribed tasks.
auto it = task_dependencies_.find(task_id);
if (it == task_dependencies_.end()) {
return false;
}
const TaskDependencies task_entry = std::move(it->second);
task_dependencies_.erase(it);
// Remove the task's dependencies.
for (const auto &object_id : task_entry.get_dependencies) {
// Get the ID of the task that creates the dependency.
TaskID creating_task_id = object_id.TaskId();
auto creating_task_entry = required_tasks_.find(creating_task_id);
// Remove the task from the list of tasks that are dependent on this
// object.
auto it = creating_task_entry->second.find(object_id);
RAY_CHECK(it != creating_task_entry->second.end());
RAY_CHECK(it->second.dependent_tasks.erase(task_id) > 0);
// If nothing else depends on the object, then erase the object entry.
if (it->second.Empty()) {
creating_task_entry->second.erase(it);
// Remove the task that creates this object if there are no more object
// dependencies created by the task.
if (creating_task_entry->second.empty()) {
required_tasks_.erase(creating_task_entry);
}
}
}
// These dependencies are no longer required by the given task. Cancel any
// in-progress operations to make them local.
for (const auto &object_id : task_entry.get_dependencies) {
HandleRemoteDependencyCanceled(object_id);
}
return true;
}
void TaskDependencyManager::UnsubscribeWaitDependencies(const WorkerID &worker_id) {
RAY_LOG(DEBUG) << "Worker " << worker_id << " no longer blocked";
// Remove the task from the table of subscribed tasks.
auto it = worker_dependencies_.find(worker_id);
if (it == worker_dependencies_.end()) {
return;
}
const WorkerDependencies worker_entry = std::move(it->second);
worker_dependencies_.erase(it);
// Remove the task's dependencies.
for (const auto &object_id : worker_entry) {
// Get the ID of the task that creates the dependency.
TaskID creating_task_id = object_id.TaskId();
auto creating_task_entry = required_tasks_.find(creating_task_id);
// Remove the worker from the list of workers that are dependent on this
// object.
auto it = creating_task_entry->second.find(object_id);
RAY_CHECK(it != creating_task_entry->second.end());
RAY_CHECK(it->second.dependent_workers.erase(worker_id) > 0);
// If nothing else depends on the object, then erase the object entry.
if (it->second.Empty()) {
creating_task_entry->second.erase(it);
// Remove the task that creates this object if there are no more object
// dependencies created by the task.
if (creating_task_entry->second.empty()) {
required_tasks_.erase(creating_task_entry);
}
}
}
// These dependencies are no longer required by the given task. Cancel any
// in-progress operations to make them local.
for (const auto &object_id : worker_entry) {
HandleRemoteDependencyCanceled(object_id);
}
}
void TaskDependencyManager::TaskPending(const Task &task) {
// Direct tasks are not tracked by the raylet.
// NOTE(zhijunfu): Direct tasks are not tracked by the raylet,
// but we still need raylet to reconstruct the actors.
// For direct actor creation task:
// - Initially the caller leases a worker from raylet and
// then pushes actor creation task directly to the worker,
// thus it doesn't need task lease. And actually if we
// acquire a lease in this case and forget to cancel it,
// the lease would never expire which will prevent the
// actor from being restarted;
// - When a direct actor is restarted, raylet resubmits
// the task, and the task can be forwarded to another raylet,
// and eventually assigned to a worker. In this case we need
// the task lease to make sure there's only one raylet can
// resubmit the task.
//
// We can use `OnDispatch` to differeniate whether this task is
// a worker lease request.
// For direct actor creation task:
// - when it's submitted by core worker, we guarantee that
// we always request a new worker lease, in that case
// `OnDispatch` is overridden to an actual callback.
// - when it's resubmitted by raylet because of reconstruction,
// `OnDispatch` will not be overridden and thus is nullptr.
if (task.GetTaskSpecification().IsActorCreationTask() && task.OnDispatch() == nullptr) {
// This is an actor creation task, and it's being restarted,
// in this case we still need the task lease. Note that we don't
// require task lease for direct actor creation task.
} else {
return;
}
TaskID task_id = task.GetTaskSpecification().TaskId();
RAY_LOG(DEBUG) << "Task execution " << task_id << " pending";
// Record that the task is pending execution.
auto inserted = pending_tasks_.insert(task_id);
if (inserted.second) {
// This is the first time we've heard that this task is pending. Find any
// subscribed tasks that are dependent on objects created by the pending
// task.
auto remote_task_entry = required_tasks_.find(task_id);
if (remote_task_entry != required_tasks_.end()) {
for (const auto &object_entry : remote_task_entry->second) {
// This object created by the pending task will appear locally once the
// task completes execution. Cancel any in-progress operations to make
// the object local.
HandleRemoteDependencyCanceled(object_entry.first);
}
}
}
}
void TaskDependencyManager::TaskCanceled(const TaskID &task_id) {
RAY_LOG(DEBUG) << "Task execution " << task_id << " canceled";
// Record that the task is no longer pending execution.
auto it = pending_tasks_.find(task_id);
if (it == pending_tasks_.end()) {
return;
}
pending_tasks_.erase(it);
// Find any subscribed tasks that are dependent on objects created by the
// canceled task.
auto remote_task_entry = required_tasks_.find(task_id);
if (remote_task_entry != required_tasks_.end()) {
for (const auto &object_entry : remote_task_entry->second) {
// This object created by the task will no longer appear locally since
// the task is canceled. Try to make the object local if necessary.
HandleRemoteDependencyRequired(object_entry.first);
}
}
}
void TaskDependencyManager::RemoveTasksAndRelatedObjects(
const std::unordered_set<TaskID> &task_ids) {
// Collect a list of all the unique objects that these tasks were subscribed
// to.
std::unordered_set<ObjectID> required_objects;
for (auto it = task_ids.begin(); it != task_ids.end(); it++) {
auto task_it = task_dependencies_.find(*it);
if (task_it != task_dependencies_.end()) {
// Add the objects that this task was subscribed to.
required_objects.insert(task_it->second.get_dependencies.begin(),
task_it->second.get_dependencies.end());
}
// The task no longer depends on anything.
task_dependencies_.erase(*it);
// The task is no longer pending execution.
pending_tasks_.erase(*it);
}
// Cancel all of the objects that were required by the removed tasks.
for (const auto &object_id : required_objects) {
TaskID creating_task_id = object_id.TaskId();
required_tasks_.erase(creating_task_id);
HandleRemoteDependencyCanceled(object_id);
}
// Make sure that the tasks in task_ids no longer have tasks dependent on
// them.
for (const auto &task_id : task_ids) {
RAY_CHECK(required_tasks_.find(task_id) == required_tasks_.end())
<< "RemoveTasksAndRelatedObjects was called on " << task_id
<< ", but another task depends on it that was not included in the argument";
}
}
std::string TaskDependencyManager::DebugString() const {
std::stringstream result;
result << "TaskDependencyManager:";
result << "\n- task dep map size: " << task_dependencies_.size();
result << "\n- task req map size: " << required_tasks_.size();
result << "\n- req objects map size: " << required_objects_.size();
result << "\n- local objects map size: " << local_objects_.size();
result << "\n- pending tasks map size: " << pending_tasks_.size();
return result.str();
}
bool TaskDependencyManager::GetOwnerAddress(const ObjectID &object_id,
rpc::Address *owner_address) const {
const auto creating_task_entry = required_tasks_.find(object_id.TaskId());
if (creating_task_entry == required_tasks_.end()) {
return false;
}
const auto it = creating_task_entry->second.find(object_id);
if (it == creating_task_entry->second.end()) {
return false;
}
*owner_address = it->second.owner_address;
return !owner_address->worker_id().empty();
}
} // namespace raylet
} // namespace ray
-260
View File
@@ -1,260 +0,0 @@
// Copyright 2017 The Ray 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.
#pragma once
// clang-format off
#include "ray/common/id.h"
#include "ray/common/task/task.h"
#include "ray/object_manager/object_manager.h"
#include "ray/raylet/reconstruction_policy.h"
// clang-format on
namespace ray {
namespace raylet {
using rpc::TaskLeaseData;
class ReconstructionPolicy;
/// Used for unit-testing the ClusterTaskManager, which calls these methods for
/// locally queued tasks that have dependencies.
class TaskDependencyManagerInterface {
public:
virtual bool SubscribeGetDependencies(
const TaskID &task_id,
const std::vector<rpc::ObjectReference> &required_objects) = 0;
virtual bool IsTaskReady(const TaskID &task_id) const = 0;
virtual bool UnsubscribeGetDependencies(const TaskID &task_id) = 0;
virtual ~TaskDependencyManagerInterface() {}
};
/// \class TaskDependencyManager
///
/// Responsible for managing object dependencies for tasks. The caller can
/// subscribe to object dependencies for a task. The task manager will
/// determine which object dependencies are remote. These are the objects that
/// are neither in the local object store, nor will they be created by a
/// locally queued task. The task manager will request that these objects be
/// made available locally, either by object transfer from a remote node or
/// reconstruction. The task manager will also cancel these objects if they are
/// no longer needed by any task.
class TaskDependencyManager : public TaskDependencyManagerInterface {
public:
/// Create a task dependency manager.
TaskDependencyManager(ObjectManagerInterface &object_manager,
ReconstructionPolicyInterface &reconstruction_policy);
/// Check whether an object is locally available.
///
/// \param object_id The object to check for.
/// \return Whether the object is local.
bool CheckObjectLocal(const ObjectID &object_id) const;
/// Subscribe to object depedencies required by the task and check whether
/// all dependencies are fulfilled. This should be called for task arguments and
/// `ray.get` calls during task execution.
///
/// The TaskDependencyManager will track the task's dependencies
/// until UnsubscribeGetDependencies is called on the same task ID. If any
/// dependencies are remote, then they will be requested. When the last
/// remote dependency later appears locally via a call to HandleObjectLocal,
/// the subscribed task will be returned by the HandleObjectLocal call,
/// signifying that it is ready to run. This method may be called multiple
/// times per task.
///
/// \param task_id The ID of the task whose dependencies to subscribe to.
/// \param required_objects The objects required by the task.
/// \return Whether all of the given dependencies for the given task are
/// local.
bool SubscribeGetDependencies(
const TaskID &task_id, const std::vector<rpc::ObjectReference> &required_objects);
/// Check whether a task is ready to run. The task ID must
/// have been previously subscribed by the caller.
///
/// \param task_id The ID of the task to check.
/// \return Whether all of the dependencies for the task are
/// local.
bool IsTaskReady(const TaskID &task_id) const;
/// Subscribe to object depedencies required by the worker. This should be called for
/// ray.wait calls during task execution.
///
/// The TaskDependencyManager will track all remote dependencies until the
/// dependencies are local, or until UnsubscribeWaitDependencies is called
/// with the same worker ID, whichever occurs first. Remote dependencies will
/// be requested. This method may be called multiple times per worker on the
/// same objects.
///
/// \param worker_id The ID of the worker that called `ray.wait`.
/// \param required_objects The objects required by the worker.
/// \return Void.
void SubscribeWaitDependencies(
const WorkerID &worker_id,
const std::vector<rpc::ObjectReference> &required_objects);
/// Unsubscribe from the object dependencies required by this task through the task
/// arguments or `ray.get`. If the objects were remote and are no longer required by any
/// subscribed task, then they will be canceled.
///
/// \param task_id The ID of the task whose dependencies we should unsubscribe from.
/// \return Whether the task was subscribed before.
bool UnsubscribeGetDependencies(const TaskID &task_id);
/// Unsubscribe from the object dependencies required by this worker through `ray.wait`.
/// If the objects were remote and are no longer required by any subscribed task, then
/// they will be canceled.
///
/// \param worker_id The ID of the worker whose dependencies we should unsubscribe from.
/// \return The objects that the worker was waiting on.
void UnsubscribeWaitDependencies(const WorkerID &worker_id);
/// Mark that the given task is pending execution. Any objects that it creates
/// are now considered to be pending creation. If there are any subscribed
/// tasks that depend on these objects, then the objects will be canceled.
///
/// \param task The task that is pending execution.
void TaskPending(const Task &task);
/// Mark that the given task is no longer pending execution. Any objects that
/// it creates that are not already local are now considered to be remote. If
/// there are any subscribed tasks that depend on these objects, then the
/// objects will be requested.
///
/// \param task_id The ID of the task to cancel.
void TaskCanceled(const TaskID &task_id);
/// Handle an object becoming locally available. If there are any subscribed
/// tasks that depend on this object, then the object will be canceled.
///
/// \param object_id The object ID of the object to mark as locally
/// available.
/// \return A list of task IDs. This contains all subscribed tasks that now
/// have all of their dependencies fulfilled, once this object was made
/// local.
std::vector<TaskID> HandleObjectLocal(const ray::ObjectID &object_id);
/// Handle an object that is no longer locally available. If there are any
/// subscribed tasks that depend on this object, then the object will be
/// requested.
///
/// \param object_id The object ID of the object that was previously locally
/// available.
/// \return A list of task IDs. This contains all subscribed tasks that
/// previously had all of their dependencies fulfilled, but are now missing
/// this object dependency.
std::vector<TaskID> HandleObjectMissing(const ray::ObjectID &object_id);
/// Remove all of the tasks specified. These tasks will no longer be
/// considered pending and the objects they depend on will no longer be
/// required.
///
/// \param task_ids The collection of task IDs. For a given task in this set,
/// all tasks that depend on the task must also be included in the set.
void RemoveTasksAndRelatedObjects(const std::unordered_set<TaskID> &task_ids);
/// Returns debug string for class.
///
/// \return string.
std::string DebugString() const;
/// Get the address of the owner of this object. An address will only be
/// returned if the caller previously specified that this object is required
/// on this node, through a call to SubscribeGetDependencies or
/// SubscribeWaitDependencies.
///
/// \param[in] object_id The object whose owner to get.
/// \param[out] owner_address The address of the object's owner, if
/// available.
/// \return True if we have owner information for the object.
bool GetOwnerAddress(const ObjectID &object_id, rpc::Address *owner_address) const;
private:
struct ObjectDependencies {
ObjectDependencies(const rpc::ObjectReference &ref)
: owner_address(ref.owner_address()) {}
/// The tasks that depend on this object, either because the object is a task argument
/// or because the task called `ray.get` on the object.
std::unordered_set<TaskID> dependent_tasks;
/// The workers that depend on this object because they called `ray.wait` on the
/// object.
std::unordered_set<WorkerID> dependent_workers;
/// The address of the worker that owns this object.
rpc::Address owner_address;
bool Empty() const { return dependent_tasks.empty() && dependent_workers.empty(); }
};
/// A struct to represent the object dependencies of a task.
struct TaskDependencies {
/// The objects that the task depends on. These are either the arguments to
/// the task or objects that the task calls `ray.get` on. These must be
/// local before the task is ready to execute. Objects are removed from
/// this set once UnsubscribeGetDependencies is called.
std::unordered_set<ObjectID> get_dependencies;
/// The number of object arguments that are not available locally. This
/// must be zero before the task is ready to execute.
int64_t num_missing_get_dependencies;
};
/// The objects that the worker is fetching. These are objects that a task that executed
/// or is executing on the worker called `ray.wait` on that are not yet local. An object
/// will be automatically removed from this set once it becomes local.
using WorkerDependencies = std::unordered_set<ObjectID>;
/// Check whether the given object needs to be made available through object
/// transfer or reconstruction. These are objects for which: (1) there is a
/// subscribed task dependent on it, (2) the object is not local, and (3) the
/// task that creates the object is not pending execution locally.
bool CheckObjectRequired(const ObjectID &object_id, rpc::Address *owner_address) const;
/// If the given object is required, then request that the object be made
/// available through object transfer or reconstruction.
void HandleRemoteDependencyRequired(const ObjectID &object_id);
/// If the given object is no longer required, then cancel any in-progress
/// operations to make the object available through object transfer or
/// reconstruction.
void HandleRemoteDependencyCanceled(const ObjectID &object_id);
/// The object manager, used to fetch required objects from remote nodes.
ObjectManagerInterface &object_manager_;
/// The reconstruction policy, used to reconstruct required objects that no
/// longer exist on any live nodes.
ReconstructionPolicyInterface &reconstruction_policy_;
/// A mapping from task ID of each subscribed task to its list of object
/// dependencies, either task arguments or objects passed into `ray.get`.
std::unordered_map<ray::TaskID, TaskDependencies> task_dependencies_;
/// A mapping from worker ID to each object that the worker called `ray.wait` on.
std::unordered_map<ray::WorkerID, WorkerDependencies> worker_dependencies_;
/// All tasks whose outputs are required by a subscribed task. This is a
/// mapping from task ID to information about the objects that the task
/// creates, either by return value or by `ray.put`. For each object, we
/// store the IDs of the subscribed tasks that are dependent on the object.
std::unordered_map<ray::TaskID, std::unordered_map<ObjectID, ObjectDependencies>>
required_tasks_;
/// Objects that are required by a subscribed task, are not local, and are
/// not created by a pending task. For these objects, there are pending
/// operations to make the object available.
std::unordered_set<ray::ObjectID> required_objects_;
/// The set of locally available objects.
std::unordered_set<ray::ObjectID> local_objects_;
/// The set of tasks that are pending execution. Any objects created by these
/// tasks that are not already local are pending creation.
std::unordered_set<ray::TaskID> pending_tasks_;
};
} // namespace raylet
} // namespace ray
@@ -1,559 +0,0 @@
// Copyright 2017 The Ray 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 "ray/raylet/task_dependency_manager.h"
#include <boost/asio.hpp>
#include <list>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ray/common/task/task_util.h"
#include "ray/common/test_util.h"
namespace ray {
namespace raylet {
using ::testing::_;
const static JobID kDefaultJobId = JobID::FromInt(1);
const static TaskID kDefaultDriverTaskId = TaskID::ForDriverTask(kDefaultJobId);
class MockObjectManager : public ObjectManagerInterface {
public:
MOCK_METHOD2(Pull,
ray::Status(const ObjectID &object_id, const rpc::Address &owner_address));
MOCK_METHOD1(CancelPull, void(const ObjectID &object_id));
};
class MockReconstructionPolicy : public ReconstructionPolicyInterface {
public:
MOCK_METHOD2(ListenAndMaybeReconstruct,
void(const ObjectID &object_id, const rpc::Address &owner_address));
MOCK_METHOD1(Cancel, void(const ObjectID &object_id));
};
class TaskDependencyManagerTest : public ::testing::Test {
public:
TaskDependencyManagerTest()
: object_manager_mock_(),
reconstruction_policy_mock_(),
task_dependency_manager_(object_manager_mock_, reconstruction_policy_mock_) {}
protected:
MockObjectManager object_manager_mock_;
MockReconstructionPolicy reconstruction_policy_mock_;
TaskDependencyManager task_dependency_manager_;
};
static inline Task ExampleTask(const std::vector<ObjectID> &arguments,
uint64_t num_returns) {
TaskSpecBuilder builder;
rpc::Address address;
builder.SetCommonTaskSpec(RandomTaskId(), "example_task", Language::PYTHON,
FunctionDescriptorBuilder::BuildPython("", "", "", ""),
JobID::Nil(), RandomTaskId(), 0, RandomTaskId(), address,
num_returns, {}, {},
std::make_pair(PlacementGroupID::Nil(), -1), true, "");
builder.SetActorCreationTaskSpec(ActorID::Nil(), 1, 1, {}, 1, false, "", false);
for (const auto &arg : arguments) {
builder.AddArg(TaskArgByReference(arg, rpc::Address()));
}
rpc::TaskExecutionSpec execution_spec_message;
execution_spec_message.set_num_forwards(1);
return Task(builder.Build(), TaskExecutionSpecification(execution_spec_message));
}
std::vector<Task> MakeTaskChain(int chain_size,
const std::vector<ObjectID> &initial_arguments,
int64_t num_returns) {
std::vector<Task> task_chain;
std::vector<ObjectID> arguments = initial_arguments;
for (int i = 0; i < chain_size; i++) {
auto task = ExampleTask(arguments, num_returns);
task_chain.push_back(task);
arguments.clear();
for (size_t j = 0; j < task.GetTaskSpecification().NumReturns(); j++) {
arguments.push_back(task.GetTaskSpecification().ReturnId(j));
}
}
return task_chain;
}
TEST_F(TaskDependencyManagerTest, TestSimpleTask) {
// Create a task with 3 arguments.
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
arguments.push_back(ObjectID::FromRandom());
}
TaskID task_id = RandomTaskId();
// No objects have been registered in the task dependency manager, so all
// arguments should be remote.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, Pull(argument_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
// Subscribe to the task's dependencies.
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task_id, ObjectIdsToRefs(arguments));
ASSERT_FALSE(ready);
// All arguments should be canceled as they become available locally.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, CancelPull(argument_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
// For each argument except the last, tell the task dependency manager that
// the argument is local.
int i = 0;
for (; i < num_arguments - 1; i++) {
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(arguments[i]);
ASSERT_TRUE(ready_task_ids.empty());
}
// Tell the task dependency manager that the last argument is local. Now the
// task should be ready to run.
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(arguments[i]);
ASSERT_EQ(ready_task_ids.size(), 1);
ASSERT_EQ(ready_task_ids.front(), task_id);
}
TEST_F(TaskDependencyManagerTest, TestDuplicateSubscribeGetDependencies) {
// Create a task with 3 arguments.
TaskID task_id = RandomTaskId();
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
// Add the new argument to the list of dependencies to subscribe to.
ObjectID argument_id = ObjectID::FromRandom();
arguments.push_back(argument_id);
// Subscribe to the task's dependencies. All arguments except the last are
// duplicates of previous subscription calls. Each argument should only be
// requested from the node manager once.
EXPECT_CALL(object_manager_mock_, Pull(argument_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task_id, ObjectIdsToRefs(arguments));
ASSERT_FALSE(ready);
}
// All arguments should be canceled as they become available locally.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, CancelPull(argument_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
// For each argument except the last, tell the task dependency manager that
// the argument is local.
int i = 0;
for (; i < num_arguments - 1; i++) {
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(arguments[i]);
ASSERT_TRUE(ready_task_ids.empty());
}
// Tell the task dependency manager that the last argument is local. Now the
// task should be ready to run.
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(arguments[i]);
ASSERT_EQ(ready_task_ids.size(), 1);
ASSERT_EQ(ready_task_ids.front(), task_id);
}
TEST_F(TaskDependencyManagerTest, TestMultipleTasks) {
// Create 3 tasks that are dependent on the same object.
ObjectID argument_id = ObjectID::FromRandom();
std::vector<TaskID> dependent_tasks;
int num_dependent_tasks = 3;
// The object should only be requested from the object manager once for all
// three tasks.
EXPECT_CALL(object_manager_mock_, Pull(argument_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
for (int i = 0; i < num_dependent_tasks; i++) {
TaskID task_id = RandomTaskId();
dependent_tasks.push_back(task_id);
// Subscribe to each of the task's dependencies.
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task_id, ObjectIdsToRefs({argument_id}));
ASSERT_FALSE(ready);
}
// Tell the task dependency manager that the object is local.
EXPECT_CALL(object_manager_mock_, CancelPull(argument_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(argument_id);
// Check that all tasks are now ready to run.
ASSERT_EQ(ready_task_ids.size(), dependent_tasks.size());
for (const auto &task_id : ready_task_ids) {
ASSERT_NE(std::find(dependent_tasks.begin(), dependent_tasks.end(), task_id),
dependent_tasks.end());
}
}
TEST_F(TaskDependencyManagerTest, TestTaskChain) {
// Create 3 tasks, each dependent on the previous. The first task has no
// arguments.
int num_tasks = 3;
auto tasks = MakeTaskChain(num_tasks, {}, 1);
int num_ready_tasks = 1;
int i = 0;
// No objects should be remote or canceled since each task depends on a
// locally queued task.
EXPECT_CALL(object_manager_mock_, Pull(_, _)).Times(0);
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _)).Times(0);
EXPECT_CALL(object_manager_mock_, CancelPull(_)).Times(0);
EXPECT_CALL(reconstruction_policy_mock_, Cancel(_)).Times(0);
for (const auto &task : tasks) {
// Subscribe to each of the tasks' arguments.
const auto &arguments = task.GetDependencies();
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task.GetTaskSpecification().TaskId(), arguments);
if (i < num_ready_tasks) {
// The first task should be ready to run since it has no arguments.
ASSERT_TRUE(ready);
} else {
// All remaining tasks depend on the previous task.
ASSERT_FALSE(ready);
}
// Mark each task as pending.
task_dependency_manager_.TaskPending(task);
i++;
}
// Simulate executing each task. Each task's completion should make the next
// task runnable.
while (!tasks.empty()) {
auto task = tasks.front();
tasks.erase(tasks.begin());
TaskID task_id = task.GetTaskSpecification().TaskId();
auto return_id = task.GetTaskSpecification().ReturnId(0);
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
// Simulate the object notifications for the task's return values.
auto ready_tasks = task_dependency_manager_.HandleObjectLocal(return_id);
if (tasks.empty()) {
// If there are no more tasks, then there should be no more tasks that
// become ready to run.
ASSERT_TRUE(ready_tasks.empty());
} else {
// If there are more tasks to run, then the next task in the chain should
// now be ready to run.
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), tasks.front().GetTaskSpecification().TaskId());
}
// Simulate the task finishing execution.
task_dependency_manager_.TaskCanceled(task_id);
}
}
TEST_F(TaskDependencyManagerTest, TestDependentPut) {
// Create a task with 3 arguments.
auto task1 = ExampleTask({}, 0);
ObjectID put_id =
ObjectID::FromIndex(task1.GetTaskSpecification().TaskId(), /*index=*/1);
auto task2 = ExampleTask({put_id}, 0);
// No objects have been registered in the task dependency manager, so the put
// object should be remote.
EXPECT_CALL(object_manager_mock_, Pull(put_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(put_id, _));
// Subscribe to the task's dependencies.
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task2.GetTaskSpecification().TaskId(), ObjectIdsToRefs({put_id}));
ASSERT_FALSE(ready);
// The put object should be considered local as soon as the task that creates
// it is pending execution.
EXPECT_CALL(object_manager_mock_, CancelPull(put_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(put_id));
task_dependency_manager_.TaskPending(task1);
}
TEST_F(TaskDependencyManagerTest, TestTaskForwarding) {
// Create 2 tasks, one dependent on the other. The first has no arguments.
int num_tasks = 2;
auto tasks = MakeTaskChain(num_tasks, {}, 1);
for (const auto &task : tasks) {
// Subscribe to each of the tasks' arguments.
const auto &arguments = task.GetDependencies();
static_cast<void>(task_dependency_manager_.SubscribeGetDependencies(
task.GetTaskSpecification().TaskId(), arguments));
task_dependency_manager_.TaskPending(task);
}
// Get the first task.
const auto task = tasks.front();
TaskID task_id = task.GetTaskSpecification().TaskId();
ObjectID return_id = task.GetTaskSpecification().ReturnId(0);
// Simulate forwarding the first task to a remote node.
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
// The object returned by the first task should be considered remote once we
// cancel the forwarded task, since the second task depends on it.
EXPECT_CALL(object_manager_mock_, Pull(return_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(return_id, _));
task_dependency_manager_.TaskCanceled(task_id);
// Simulate the task executing on a remote node and its return value
// appearing locally.
EXPECT_CALL(object_manager_mock_, CancelPull(return_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(return_id));
auto ready_tasks = task_dependency_manager_.HandleObjectLocal(return_id);
// Check that the task that we kept is now ready to run.
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), tasks.back().GetTaskSpecification().TaskId());
}
TEST_F(TaskDependencyManagerTest, TestEviction) {
// Create a task with 3 arguments.
int num_arguments = 3;
std::vector<ObjectID> arguments;
for (int i = 0; i < num_arguments; i++) {
arguments.push_back(ObjectID::FromRandom());
}
TaskID task_id = RandomTaskId();
// No objects have been registered in the task dependency manager, so all
// arguments should be remote.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, Pull(argument_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
// Subscribe to the task's dependencies.
bool ready = task_dependency_manager_.SubscribeGetDependencies(
task_id, ObjectIdsToRefs(arguments));
ASSERT_FALSE(ready);
// Tell the task dependency manager that each of the arguments is now
// available.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, CancelPull(argument_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> ready_tasks;
ready_tasks = task_dependency_manager_.HandleObjectLocal(arguments[i]);
if (i == arguments.size() - 1) {
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), task_id);
} else {
ASSERT_TRUE(ready_tasks.empty());
}
}
// Simulate each of the arguments getting evicted. Each object should now be
// considered remote.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, Pull(argument_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(argument_id, _));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> waiting_tasks;
waiting_tasks = task_dependency_manager_.HandleObjectMissing(arguments[i]);
if (i == 0) {
// The first eviction should cause the task to go back to the waiting
// state.
ASSERT_EQ(waiting_tasks.size(), 1);
ASSERT_EQ(waiting_tasks.front(), task_id);
} else {
// The subsequent evictions shouldn't cause any more tasks to go back to
// the waiting state.
ASSERT_TRUE(waiting_tasks.empty());
}
}
// Tell the task dependency manager that each of the arguments is available
// again.
for (const auto &argument_id : arguments) {
EXPECT_CALL(object_manager_mock_, CancelPull(argument_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(argument_id));
}
for (size_t i = 0; i < arguments.size(); i++) {
std::vector<TaskID> ready_tasks;
ready_tasks = task_dependency_manager_.HandleObjectLocal(arguments[i]);
if (i == arguments.size() - 1) {
ASSERT_EQ(ready_tasks.size(), 1);
ASSERT_EQ(ready_tasks.front(), task_id);
} else {
ASSERT_TRUE(ready_tasks.empty());
}
}
}
TEST_F(TaskDependencyManagerTest, TestRemoveTasksAndRelatedObjects) {
// Create 3 tasks, each dependent on the previous. The first task has no
// arguments.
int num_tasks = 3;
auto tasks = MakeTaskChain(num_tasks, {}, 1);
// No objects should be remote or canceled since each task depends on a
// locally queued task.
EXPECT_CALL(object_manager_mock_, Pull(_, _)).Times(0);
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _)).Times(0);
EXPECT_CALL(object_manager_mock_, CancelPull(_)).Times(0);
EXPECT_CALL(reconstruction_policy_mock_, Cancel(_)).Times(0);
for (const auto &task : tasks) {
// Subscribe to each of the tasks' arguments.
const auto &arguments = task.GetDependencies();
task_dependency_manager_.SubscribeGetDependencies(
task.GetTaskSpecification().TaskId(), arguments);
// Mark each task as pending.
task_dependency_manager_.TaskPending(task);
}
// Simulate executing the first task. This should make the second task
// runnable.
auto task = tasks.front();
TaskID task_id = task.GetTaskSpecification().TaskId();
auto return_id = task.GetTaskSpecification().ReturnId(0);
task_dependency_manager_.UnsubscribeGetDependencies(task_id);
// Simulate the object notifications for the task's return values.
auto ready_tasks = task_dependency_manager_.HandleObjectLocal(return_id);
// The second task should be ready to run.
ASSERT_EQ(ready_tasks.size(), 1);
// Simulate the task finishing execution.
task_dependency_manager_.TaskCanceled(task_id);
// Remove all tasks from the manager except the first task, which already
// finished executing.
std::unordered_set<TaskID> task_ids;
for (const auto &task : tasks) {
task_ids.insert(task.GetTaskSpecification().TaskId());
}
task_ids.erase(task_id);
task_dependency_manager_.RemoveTasksAndRelatedObjects(task_ids);
// Simulate evicting the return value of the first task. Make sure that this
// does not return the second task, which should have been removed.
auto waiting_tasks = task_dependency_manager_.HandleObjectMissing(return_id);
ASSERT_TRUE(waiting_tasks.empty());
// Simulate the object notifications for the second task's return values.
// Make sure that this does not return the third task, which should have been
// removed.
return_id = tasks[1].GetTaskSpecification().ReturnId(0);
ready_tasks = task_dependency_manager_.HandleObjectLocal(return_id);
ASSERT_TRUE(ready_tasks.empty());
}
/// Test that when no objects are locally available, a `ray.wait` call makes
/// the correct requests to remote nodes and correctly cancels the requests
/// when the `ray.wait` call is canceled.
TEST_F(TaskDependencyManagerTest, TestWaitDependencies) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> wait_object_ids;
for (int i = 0; i < num_objects; i++) {
wait_object_ids.push_back(ObjectID::FromRandom());
}
// Simulate a worker calling `ray.wait` on some objects.
EXPECT_CALL(object_manager_mock_, Pull(_, _)).Times(num_objects);
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _))
.Times(num_objects);
task_dependency_manager_.SubscribeWaitDependencies(worker_id,
ObjectIdsToRefs(wait_object_ids));
// Check that it's okay to call `ray.wait` on the same objects again. No new
// calls should be made to try and make the objects local.
task_dependency_manager_.SubscribeWaitDependencies(worker_id,
ObjectIdsToRefs(wait_object_ids));
// Cancel the worker's `ray.wait`. calls.
EXPECT_CALL(object_manager_mock_, CancelPull(_)).Times(num_objects);
EXPECT_CALL(reconstruction_policy_mock_, Cancel(_)).Times(num_objects);
task_dependency_manager_.UnsubscribeWaitDependencies(worker_id);
}
/// Test that when one of the objects is already local at the time of the
/// `ray.wait` call, the `ray.wait` call does not trigger any requests to
/// remote nodes for that object.
TEST_F(TaskDependencyManagerTest, TestWaitDependenciesObjectLocal) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> wait_object_ids;
for (int i = 0; i < num_objects; i++) {
wait_object_ids.push_back(ObjectID::FromRandom());
}
// Simulate one of the objects becoming local. The later `ray.wait` call
// should have no effect because the object is already local.
const ObjectID local_object_id = std::move(wait_object_ids.back());
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(local_object_id);
ASSERT_TRUE(ready_task_ids.empty());
// Simulate a worker calling `ray.wait` on the objects. It should only make
// requests for the objects that are not local.
for (const auto &object_id : wait_object_ids) {
if (object_id != local_object_id) {
EXPECT_CALL(object_manager_mock_, Pull(object_id, _));
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(object_id, _));
}
}
task_dependency_manager_.SubscribeWaitDependencies(worker_id,
ObjectIdsToRefs(wait_object_ids));
// Simulate the local object getting evicted. The `ray.wait` call should not
// be reactivated.
auto waiting_task_ids = task_dependency_manager_.HandleObjectMissing(local_object_id);
ASSERT_TRUE(waiting_task_ids.empty());
// Simulate a worker calling `ray.wait` on the objects. It should only make
// requests for the objects that are not local.
for (const auto &object_id : wait_object_ids) {
if (object_id != local_object_id) {
EXPECT_CALL(object_manager_mock_, CancelPull(object_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(object_id));
}
}
task_dependency_manager_.UnsubscribeWaitDependencies(worker_id);
}
/// Test that when one of the objects becomes local after a `ray.wait` call,
/// all requests to remote nodes associated with the object are canceled.
TEST_F(TaskDependencyManagerTest, TestWaitDependenciesHandleObjectLocal) {
// Generate a random worker and objects to wait on.
WorkerID worker_id = WorkerID::FromRandom();
int num_objects = 3;
std::vector<ObjectID> wait_object_ids;
for (int i = 0; i < num_objects; i++) {
wait_object_ids.push_back(ObjectID::FromRandom());
}
// Simulate a worker calling `ray.wait` on some objects.
EXPECT_CALL(object_manager_mock_, Pull(_, _)).Times(num_objects);
EXPECT_CALL(reconstruction_policy_mock_, ListenAndMaybeReconstruct(_, _))
.Times(num_objects);
task_dependency_manager_.SubscribeWaitDependencies(worker_id,
ObjectIdsToRefs(wait_object_ids));
// Simulate one of the objects becoming local while the `ray.wait` calls is
// active. The `ray.wait` call should be canceled.
const ObjectID local_object_id = std::move(wait_object_ids.back());
wait_object_ids.pop_back();
EXPECT_CALL(object_manager_mock_, CancelPull(local_object_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(local_object_id));
auto ready_task_ids = task_dependency_manager_.HandleObjectLocal(local_object_id);
ASSERT_TRUE(ready_task_ids.empty());
// Simulate the local object getting evicted. The `ray.wait` call should not
// be reactivated.
auto waiting_task_ids = task_dependency_manager_.HandleObjectMissing(local_object_id);
ASSERT_TRUE(waiting_task_ids.empty());
// Cancel the worker's `ray.wait` calls. Only the objects that are still not
// local should be canceled.
for (const auto &object_id : wait_object_ids) {
EXPECT_CALL(object_manager_mock_, CancelPull(object_id));
EXPECT_CALL(reconstruction_policy_mock_, Cancel(object_id));
}
task_dependency_manager_.UnsubscribeWaitDependencies(worker_id);
}
} // namespace raylet
} // namespace ray
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}