[direct call] Fix max_calls interaction with background tasks. (#6536)

This commit is contained in:
Eric Liang
2019-12-19 13:48:32 -08:00
committed by GitHub
parent 41fa2e9604
commit e556b729c2
15 changed files with 154 additions and 28 deletions
+8 -7
View File
@@ -688,10 +688,9 @@ cdef execute_task(
# If we've reached the max number of executions for this worker, exit.
task_counter = manager.get_task_counter(job_id, function_descriptor)
if task_counter == execution_info.max_calls:
# Intentionally disconnect so the raylet doesn't print an error.
# TODO(edoakes): we should handle max_calls in the core worker.
worker.core_worker.disconnect()
sys.exit(0)
exit = SystemExit(0)
exit.is_ray_terminate = True
raise exit
cdef CRayStatus task_execution_handler(
@@ -721,11 +720,13 @@ cdef CRayStatus task_execution_handler(
job_id=None)
sys.exit(1)
except SystemExit as e:
if not hasattr(e, "is_ray_terminate"):
logger.exception("SystemExit was raised from the worker")
# Tell the core worker to exit as soon as the result objects
# are processed.
return CRayStatus.SystemExit()
if hasattr(e, "is_ray_terminate"):
return CRayStatus.IntentionalSystemExit()
else:
logger.exception("SystemExit was raised from the worker")
return CRayStatus.UnexpectedSystemExit()
return CRayStatus.OK()
+4 -1
View File
@@ -80,7 +80,10 @@ cdef extern from "ray/common/status.h" namespace "ray" nogil:
CRayStatus Interrupted(const c_string &msg)
@staticmethod
CRayStatus SystemExit()
CRayStatus IntentionalSystemExit()
@staticmethod
CRayStatus UnexpectedSystemExit()
c_bool ok()
c_bool IsOutOfMemory()
+19
View File
@@ -91,6 +91,25 @@ def test_simple_serialization(ray_start_regular):
assert type(obj) == type(new_obj_2)
def test_background_tasks_with_max_calls(shutdown_only):
ray.init(num_cpus=2)
@ray.remote
def g():
time.sleep(.1)
return 0
@ray.remote(max_calls=1, max_retries=0)
def f():
return [g.remote()]
nested = ray.get([f.remote() for _ in range(10)])
# Should still be able to retrieve these objects, since f's workers will
# wait for g to finish before exiting.
ray.get([x[0] for x in nested])
def test_fair_queueing(shutdown_only):
ray.init(
num_cpus=1, _internal_config=json.dumps({
+15 -4
View File
@@ -81,7 +81,8 @@ enum class StatusCode : char {
RedisError = 11,
TimedOut = 12,
Interrupted = 13,
SystemExit = 14,
IntentionalSystemExit = 14,
UnexpectedSystemExit = 15,
};
#if defined(__clang__)
@@ -153,8 +154,12 @@ class RAY_EXPORT Status {
return Status(StatusCode::Interrupted, msg);
}
static Status SystemExit() {
return Status(StatusCode::SystemExit, "process requested exit");
static Status IntentionalSystemExit() {
return Status(StatusCode::IntentionalSystemExit, "intentional system exit");
}
static Status UnexpectedSystemExit() {
return Status(StatusCode::UnexpectedSystemExit, "user code caused exit");
}
// Returns true iff the status indicates success.
@@ -172,7 +177,13 @@ class RAY_EXPORT Status {
bool IsRedisError() const { return code() == StatusCode::RedisError; }
bool IsTimedOut() const { return code() == StatusCode::TimedOut; }
bool IsInterrupted() const { return code() == StatusCode::Interrupted; }
bool IsSystemExit() const { return code() == StatusCode::SystemExit; }
bool IsSystemExit() const {
return code() == StatusCode::IntentionalSystemExit ||
code() == StatusCode::UnexpectedSystemExit;
}
bool IsIntentionalSystemExit() const {
return code() == StatusCode::IntentionalSystemExit;
}
// Return a string representation of this status suitable for printing.
// Returns the string "OK" for success.
+14 -1
View File
@@ -117,7 +117,20 @@ CoreWorker::CoreWorker(const WorkerType worker_type, const Language language,
RAY_CHECK(task_execution_callback_ != nullptr);
auto execute_task = std::bind(&CoreWorker::ExecuteTask, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3);
auto exit = std::bind(&CoreWorker::Shutdown, this);
auto exit = [this](bool intentional) {
// Release the resources early in case draining takes a long time.
RAY_CHECK_OK(local_raylet_client_->NotifyDirectCallTaskBlocked());
task_manager_->DrainAndShutdown([this, intentional]() {
// To avoid problems, make sure shutdown is always called from the same
// event loop each time.
task_execution_service_.post([this, intentional]() {
if (intentional) {
Disconnect(); // Notify the raylet this is an intentional exit.
}
Shutdown();
});
});
};
raylet_task_receiver_ =
std::unique_ptr<CoreWorkerRayletTaskReceiver>(new CoreWorkerRayletTaskReceiver(
worker_context_.GetWorkerID(), local_raylet_client_, execute_task, exit));
+2 -2
View File
@@ -20,8 +20,8 @@ void FutureResolver::ResolveFutureAsync(const ObjectID &object_id, const TaskID
request,
[this, object_id](const Status &status, const rpc::GetObjectStatusReply &reply) {
if (!status.ok()) {
RAY_LOG(ERROR) << "Error retrieving the value of object ID " << object_id
<< " that was deserialized: " << status.ToString();
RAY_LOG(WARNING) << "Error retrieving the value of object ID " << object_id
<< " that was deserialized: " << status.ToString();
}
// Either the owner is gone or the owner replied that the object has
// been created. In both cases, we can now try to fetch the object via
+24
View File
@@ -17,6 +17,18 @@ void TaskManager::AddPendingTask(const TaskSpecification &spec, int max_retries)
RAY_CHECK(pending_tasks_.emplace(spec.TaskId(), std::move(entry)).second);
}
void TaskManager::DrainAndShutdown(std::function<void()> shutdown) {
absl::MutexLock lock(&mu_);
if (pending_tasks_.empty()) {
shutdown();
} else {
RAY_LOG(WARNING)
<< "This worker is still managing " << pending_tasks_.size()
<< " in flight tasks, waiting for them to finish before shutting down.";
}
shutdown_hook_ = shutdown;
}
bool TaskManager::IsTaskPending(const TaskID &task_id) const {
absl::MutexLock lock(&mu_);
return pending_tasks_.count(task_id) > 0;
@@ -68,6 +80,8 @@ void TaskManager::CompletePendingTask(const TaskID &task_id,
RAY_CHECK(actor_addr != nullptr);
actor_manager_->PublishCreatedActor(spec, *actor_addr);
}
ShutdownIfNeeded();
}
void TaskManager::PendingTaskFailed(const TaskID &task_id, rpc::ErrorType error_type,
@@ -122,6 +136,16 @@ void TaskManager::PendingTaskFailed(const TaskID &task_id, rpc::ErrorType error_
}
MarkPendingTaskFailed(task_id, spec, error_type);
}
ShutdownIfNeeded();
}
void TaskManager::ShutdownIfNeeded() {
absl::MutexLock lock(&mu_);
if (shutdown_hook_ && pending_tasks_.empty()) {
RAY_LOG(WARNING) << "All in flight tasks finished, shutting down worker.";
shutdown_hook_();
}
}
void TaskManager::MarkPendingTaskFailed(const TaskID &task_id,
+11
View File
@@ -43,6 +43,11 @@ class TaskManager : public TaskFinisherInterface {
/// \return Void.
void AddPendingTask(const TaskSpecification &spec, int max_retries = 0);
/// Wait for all pending tasks to finish, and then shutdown.
///
/// \param shutdown The shutdown callback to call.
void DrainAndShutdown(std::function<void()> shutdown);
/// Return whether the task is pending.
///
/// \param[in] task_id ID of the task to query.
@@ -76,6 +81,9 @@ class TaskManager : public TaskFinisherInterface {
void MarkPendingTaskFailed(const TaskID &task_id, const TaskSpecification &spec,
rpc::ErrorType error_type) LOCKS_EXCLUDED(mu_);
/// Shutdown if all tasks are finished and shutdown is scheduled.
void ShutdownIfNeeded() LOCKS_EXCLUDED(mu_);
/// Used to store task results.
std::shared_ptr<CoreWorkerMemoryStore> in_memory_store_;
@@ -104,6 +112,9 @@ class TaskManager : public TaskFinisherInterface {
/// storing a shared_ptr to a PushTaskRequest protobuf for all tasks.
absl::flat_hash_map<TaskID, std::pair<TaskSpecification, int>> pending_tasks_
GUARDED_BY(mu_);
/// Optional shutdown hook to call when pending tasks all finish.
std::function<void()> shutdown_hook_ GUARDED_BY(mu_) = nullptr;
};
} // namespace ray
@@ -24,12 +24,16 @@ class MockWorkerClient : public rpc::CoreWorkerClientInterface {
return Status::OK();
}
bool ReplyPushTask(Status status = Status::OK()) {
bool ReplyPushTask(Status status = Status::OK(), bool exit = false) {
if (callbacks.size() == 0) {
return false;
}
auto callback = callbacks.front();
callback(status, rpc::PushTaskReply());
auto reply = rpc::PushTaskReply();
if (exit) {
reply.set_worker_exiting(true);
}
callback(status, reply);
callbacks.pop_front();
return true;
}
@@ -412,6 +416,33 @@ TEST(DirectTaskTransportTest, TestWorkerNotReusedOnError) {
ASSERT_EQ(task_finisher->num_tasks_failed, 1);
}
TEST(DirectTaskTransportTest, TestWorkerNotReturnedOnExit) {
auto raylet_client = std::make_shared<MockRayletClient>();
auto worker_client = std::make_shared<MockWorkerClient>();
auto store = std::make_shared<CoreWorkerMemoryStore>();
auto factory = [&](const std::string &addr, int port) { return worker_client; };
auto task_finisher = std::make_shared<MockTaskFinisher>();
CoreWorkerDirectTaskSubmitter submitter(raylet_client, factory, nullptr, store,
task_finisher, ClientID::Nil(), kLongTimeout);
std::unordered_map<std::string, double> empty_resources;
std::vector<std::string> empty_descriptor;
TaskSpecification task1 = BuildTaskSpec(empty_resources, empty_descriptor);
ASSERT_TRUE(submitter.SubmitTask(task1).ok());
ASSERT_EQ(raylet_client->num_workers_requested, 1);
// Task 1 is pushed.
ASSERT_TRUE(raylet_client->GrantWorkerLease("localhost", 1000, ClientID::Nil()));
ASSERT_EQ(worker_client->callbacks.size(), 1);
// Task 1 finishes with exit status; the worker is not returned.
ASSERT_TRUE(worker_client->ReplyPushTask(Status::OK(), /*exit=*/true));
ASSERT_EQ(raylet_client->num_workers_returned, 0);
ASSERT_EQ(raylet_client->num_workers_disconnected, 0);
ASSERT_EQ(task_finisher->num_tasks_complete, 1);
ASSERT_EQ(task_finisher->num_tasks_failed, 0);
}
TEST(DirectTaskTransportTest, TestSpillback) {
auto raylet_client = std::make_shared<MockRayletClient>();
auto worker_client = std::make_shared<MockWorkerClient>();
@@ -257,16 +257,21 @@ void CoreWorkerDirectTaskReceiver::HandlePushTask(
}
}
if (status.IsSystemExit()) {
// Don't allow the worker to be reused, even though the reply status is OK.
// The worker will be shutting down shortly.
reply->set_worker_exiting(true);
// In Python, SystemExit can only be raised on the main thread. To
// work around this when we are executing tasks on worker threads,
// we re-post the exit event explicitly on the main thread.
exiting_ = true;
if (objects_valid) {
// This happens when max_calls is hit. We still need to return the objects.
send_reply_callback(Status::OK(), nullptr, nullptr);
} else {
send_reply_callback(Status::SystemExit(), nullptr, nullptr);
send_reply_callback(status, nullptr, nullptr);
}
task_main_io_service_.post([this]() { exit_handler_(); });
task_main_io_service_.post(
[this, status]() { exit_handler_(status.IsIntentionalSystemExit()); });
} else {
RAY_CHECK(objects_valid) << return_objects.size() << " " << num_returns;
send_reply_callback(status, nullptr, nullptr);
@@ -423,7 +423,7 @@ class CoreWorkerDirectTaskReceiver {
CoreWorkerDirectTaskReceiver(WorkerContext &worker_context,
boost::asio::io_service &main_io_service,
const TaskHandler &task_handler,
const std::function<void()> &exit_handler)
const std::function<void(bool)> &exit_handler)
: worker_context_(worker_context),
task_handler_(task_handler),
exit_handler_(exit_handler),
@@ -470,7 +470,7 @@ class CoreWorkerDirectTaskReceiver {
/// The callback function to process a task.
TaskHandler task_handler_;
/// The callback function to exit the worker.
std::function<void()> exit_handler_;
std::function<void(bool)> exit_handler_;
/// The IO event loop for running tasks on.
boost::asio::io_service &task_main_io_service_;
/// Factory for producing new core worker clients.
@@ -175,10 +175,16 @@ void CoreWorkerDirectTaskSubmitter::PushNormalTask(
std::move(request),
[this, task_id, is_actor, is_actor_creation, scheduling_key, addr,
assigned_resources](Status status, const rpc::PushTaskReply &reply) {
// Successful actor creation leases the worker indefinitely from the raylet.
if (!status.ok() || !is_actor_creation) {
if (reply.worker_exiting()) {
// The worker is draining and will shutdown after it is done. Don't return
// it to the Raylet since that will kill it early.
absl::MutexLock lock(&mu_);
OnWorkerIdle(addr, scheduling_key, /*error=*/!status.ok(), assigned_resources);
worker_to_lease_client_.erase(addr);
} else if (!status.ok() || !is_actor_creation) {
// Successful actor creation leases the worker indefinitely from the raylet.
absl::MutexLock lock(&mu_);
OnWorkerIdle(addr, scheduling_key,
/*error=*/!status.ok(), assigned_resources);
}
if (!status.ok()) {
// TODO: It'd be nice to differentiate here between process vs node
@@ -7,7 +7,7 @@ namespace ray {
CoreWorkerRayletTaskReceiver::CoreWorkerRayletTaskReceiver(
const WorkerID &worker_id, std::shared_ptr<raylet::RayletClient> &raylet_client,
const TaskHandler &task_handler, const std::function<void()> &exit_handler)
const TaskHandler &task_handler, const std::function<void(bool)> &exit_handler)
: worker_id_(worker_id),
raylet_client_(raylet_client),
task_handler_(task_handler),
@@ -50,7 +50,7 @@ void CoreWorkerRayletTaskReceiver::HandleAssignTask(
std::vector<std::shared_ptr<RayObject>> results;
auto status = task_handler_(task_spec, resource_ids, &results);
if (status.IsSystemExit()) {
exit_handler_();
exit_handler_(status.IsIntentionalSystemExit());
return;
}
@@ -19,7 +19,7 @@ class CoreWorkerRayletTaskReceiver {
CoreWorkerRayletTaskReceiver(const WorkerID &worker_id,
std::shared_ptr<raylet::RayletClient> &raylet_client,
const TaskHandler &task_handler,
const std::function<void()> &exit_handler);
const std::function<void(bool)> &exit_handler);
/// Handle a `AssignTask` request.
/// The implementation can handle this request asynchronously. When handling is done,
@@ -41,7 +41,7 @@ class CoreWorkerRayletTaskReceiver {
/// The callback function to process a task.
TaskHandler task_handler_;
/// The callback function to exit the worker.
std::function<void()> exit_handler_;
std::function<void(bool)> exit_handler_;
/// The callback to process arg wait complete.
std::function<void(int64_t)> on_wait_complete_;
};
+2
View File
@@ -86,6 +86,8 @@ message PushTaskRequest {
message PushTaskReply {
// The returned objects.
repeated ReturnObject return_objects = 1;
// Set to true if the worker will be exiting.
bool worker_exiting = 2;
}
message DirectActorCallArgWaitCompleteRequest {