330 lines
12 KiB
Python
330 lines
12 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import socket
|
|
from threading import RLock
|
|
|
|
from filelock import FileLock
|
|
|
|
from ray.autoscaler._private.local.config import (
|
|
LOCAL_CLUSTER_NODE_TYPE,
|
|
bootstrap_local,
|
|
get_lock_path,
|
|
get_state_path,
|
|
)
|
|
from ray.autoscaler.node_provider import NodeProvider
|
|
from ray.autoscaler.tags import (
|
|
NODE_KIND_HEAD,
|
|
NODE_KIND_WORKER,
|
|
STATUS_UP_TO_DATE,
|
|
TAG_RAY_NODE_KIND,
|
|
TAG_RAY_NODE_NAME,
|
|
TAG_RAY_NODE_STATUS,
|
|
TAG_RAY_USER_NODE_TYPE,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
filelock_logger = logging.getLogger("filelock")
|
|
filelock_logger.setLevel(logging.WARNING)
|
|
|
|
|
|
class ClusterState:
|
|
def __init__(self, lock_path, save_path, provider_config):
|
|
self.lock = RLock()
|
|
os.makedirs(os.path.dirname(lock_path), exist_ok=True)
|
|
self.file_lock = FileLock(lock_path)
|
|
self.save_path = save_path
|
|
|
|
with self.lock:
|
|
with self.file_lock:
|
|
if os.path.exists(self.save_path):
|
|
workers = json.loads(open(self.save_path).read())
|
|
head_config = workers.get(provider_config["head_ip"])
|
|
if (
|
|
not head_config
|
|
or head_config.get("tags", {}).get(TAG_RAY_NODE_KIND)
|
|
!= NODE_KIND_HEAD
|
|
):
|
|
workers = {}
|
|
logger.info("Head IP changed - recreating cluster.")
|
|
else:
|
|
workers = {}
|
|
logger.info(
|
|
"ClusterState: Loaded cluster state: {}".format(list(workers))
|
|
)
|
|
for worker_ip in provider_config["worker_ips"]:
|
|
if worker_ip not in workers:
|
|
workers[worker_ip] = {
|
|
"tags": {TAG_RAY_NODE_KIND: NODE_KIND_WORKER},
|
|
"state": "terminated",
|
|
}
|
|
else:
|
|
assert (
|
|
workers[worker_ip]["tags"][TAG_RAY_NODE_KIND]
|
|
== NODE_KIND_WORKER
|
|
)
|
|
if provider_config["head_ip"] not in workers:
|
|
workers[provider_config["head_ip"]] = {
|
|
"tags": {TAG_RAY_NODE_KIND: NODE_KIND_HEAD},
|
|
"state": "terminated",
|
|
}
|
|
else:
|
|
assert (
|
|
workers[provider_config["head_ip"]]["tags"][TAG_RAY_NODE_KIND]
|
|
== NODE_KIND_HEAD
|
|
)
|
|
# Relevant when a user reduces the number of workers
|
|
# without changing the headnode.
|
|
list_of_node_ips = list(provider_config["worker_ips"])
|
|
list_of_node_ips.append(provider_config["head_ip"])
|
|
for worker_ip in list(workers):
|
|
if worker_ip not in list_of_node_ips:
|
|
del workers[worker_ip]
|
|
|
|
# Set external head ip, if provided by user.
|
|
# Necessary if calling `ray up` from outside the network.
|
|
# Refer to LocalNodeProvider.external_ip function.
|
|
external_head_ip = provider_config.get("external_head_ip")
|
|
if external_head_ip:
|
|
head = workers[provider_config["head_ip"]]
|
|
head["external_ip"] = external_head_ip
|
|
|
|
assert len(workers) == len(provider_config["worker_ips"]) + 1
|
|
with open(self.save_path, "w") as f:
|
|
logger.debug(
|
|
"ClusterState: Writing cluster state: {}".format(workers)
|
|
)
|
|
f.write(json.dumps(workers))
|
|
|
|
def get(self):
|
|
with self.lock:
|
|
with self.file_lock:
|
|
workers = json.loads(open(self.save_path).read())
|
|
return workers
|
|
|
|
def put(self, worker_id, info):
|
|
assert "tags" in info
|
|
assert "state" in info
|
|
with self.lock:
|
|
with self.file_lock:
|
|
workers = self.get()
|
|
workers[worker_id] = info
|
|
with open(self.save_path, "w") as f:
|
|
logger.info(
|
|
"ClusterState: "
|
|
"Writing cluster state: {}".format(list(workers))
|
|
)
|
|
f.write(json.dumps(workers))
|
|
|
|
|
|
class OnPremCoordinatorState(ClusterState):
|
|
"""Generates & updates the state file of CoordinatorSenderNodeProvider.
|
|
|
|
Unlike ClusterState, which generates a cluster specific file with
|
|
predefined head and worker ips, OnPremCoordinatorState overwrites
|
|
ClusterState's __init__ function to generate and manage a unified
|
|
file of the status of all the nodes for multiple clusters.
|
|
"""
|
|
|
|
def __init__(self, lock_path, save_path, list_of_node_ips):
|
|
self.lock = RLock()
|
|
self.file_lock = FileLock(lock_path)
|
|
self.save_path = save_path
|
|
|
|
with self.lock:
|
|
with self.file_lock:
|
|
if os.path.exists(self.save_path):
|
|
nodes = json.loads(open(self.save_path).read())
|
|
else:
|
|
nodes = {}
|
|
logger.info(
|
|
"OnPremCoordinatorState: "
|
|
"Loaded on prem coordinator state: {}".format(nodes)
|
|
)
|
|
|
|
# Filter removed node ips.
|
|
for node_ip in list(nodes):
|
|
if node_ip not in list_of_node_ips:
|
|
del nodes[node_ip]
|
|
|
|
for node_ip in list_of_node_ips:
|
|
if node_ip not in nodes:
|
|
nodes[node_ip] = {
|
|
"tags": {},
|
|
"state": "terminated",
|
|
}
|
|
assert len(nodes) == len(list_of_node_ips)
|
|
with open(self.save_path, "w") as f:
|
|
logger.info(
|
|
"OnPremCoordinatorState: "
|
|
"Writing on prem coordinator state: {}".format(nodes)
|
|
)
|
|
f.write(json.dumps(nodes))
|
|
|
|
|
|
class LocalNodeProvider(NodeProvider):
|
|
"""NodeProvider for private/local clusters.
|
|
|
|
`node_id` is overloaded to also be `node_ip` in this class.
|
|
|
|
When `cluster_name` is provided, it manages a single cluster in a cluster
|
|
specific state file. But when `cluster_name` is None, it manages multiple
|
|
clusters in a unified state file that requires each node to be tagged with
|
|
TAG_RAY_CLUSTER_NAME in create and non_terminated_nodes function calls to
|
|
associate each node with the right cluster.
|
|
|
|
The current use case of managing multiple clusters is by
|
|
OnPremCoordinatorServer which receives node provider HTTP requests
|
|
from CoordinatorSenderNodeProvider and uses LocalNodeProvider to get
|
|
the responses.
|
|
"""
|
|
|
|
def __init__(self, provider_config, cluster_name):
|
|
NodeProvider.__init__(self, provider_config, cluster_name)
|
|
|
|
if cluster_name:
|
|
lock_path = get_lock_path(cluster_name)
|
|
state_path = get_state_path(cluster_name)
|
|
self.state = ClusterState(
|
|
lock_path,
|
|
state_path,
|
|
provider_config,
|
|
)
|
|
self.use_coordinator = False
|
|
else:
|
|
# LocalNodeProvider with a coordinator server.
|
|
self.state = OnPremCoordinatorState(
|
|
"/tmp/coordinator.lock",
|
|
"/tmp/coordinator.state",
|
|
provider_config["list_of_node_ips"],
|
|
)
|
|
self.use_coordinator = True
|
|
|
|
def non_terminated_nodes(self, tag_filters):
|
|
workers = self.state.get()
|
|
matching_ips = []
|
|
for worker_ip, info in workers.items():
|
|
if info["state"] == "terminated":
|
|
continue
|
|
ok = True
|
|
for k, v in tag_filters.items():
|
|
if info["tags"].get(k) != v:
|
|
ok = False
|
|
break
|
|
if ok:
|
|
matching_ips.append(worker_ip)
|
|
return matching_ips
|
|
|
|
def nodes_for_teardown(self, tag_filters):
|
|
"""Return all known node ids matching tag_filters regardless of state.
|
|
|
|
The local state file on the machine invoking ``ray down`` may show
|
|
workers as terminated because only the head node's autoscaler updates
|
|
them to running. During teardown we still need to reach these nodes
|
|
to stop their Docker containers.
|
|
|
|
Nodes that have already been fully torn down (i.e. terminate_node was
|
|
called, setting teardown_complete) are excluded so they are not
|
|
targeted again on subsequent teardown passes.
|
|
"""
|
|
workers = self.state.get()
|
|
return [
|
|
worker_ip
|
|
for worker_ip, info in workers.items()
|
|
if all(info["tags"].get(k) == v for k, v in tag_filters.items())
|
|
and not info.get("teardown_complete", False)
|
|
]
|
|
|
|
def is_running(self, node_id):
|
|
return self.state.get()[node_id]["state"] == "running"
|
|
|
|
def is_terminated(self, node_id):
|
|
return not self.is_running(node_id)
|
|
|
|
def node_tags(self, node_id):
|
|
return self.state.get()[node_id]["tags"]
|
|
|
|
def external_ip(self, node_id):
|
|
"""Returns an external ip if the user has supplied one.
|
|
Otherwise, use the same logic as internal_ip below.
|
|
|
|
This can be used to call ray up from outside the network, for example
|
|
if the Ray cluster exists in an AWS VPC and we're interacting with
|
|
the cluster from a laptop (where using an internal_ip will not work).
|
|
|
|
Useful for debugging the local node provider with cloud VMs."""
|
|
|
|
node_state = self.state.get()[node_id]
|
|
ext_ip = node_state.get("external_ip")
|
|
if ext_ip:
|
|
return ext_ip
|
|
else:
|
|
return socket.gethostbyname(node_id)
|
|
|
|
def internal_ip(self, node_id):
|
|
return socket.gethostbyname(node_id)
|
|
|
|
def set_node_tags(self, node_id, tags):
|
|
with self.state.lock:
|
|
with self.state.file_lock:
|
|
info = self.state.get()[node_id]
|
|
info["tags"].update(tags)
|
|
self.state.put(node_id, info)
|
|
|
|
def create_node(self, node_config, tags, count):
|
|
"""Creates min(count, currently available) nodes."""
|
|
node_type = tags[TAG_RAY_NODE_KIND]
|
|
with self.state.lock:
|
|
with self.state.file_lock:
|
|
workers = self.state.get()
|
|
for node_id, info in workers.items():
|
|
if info["state"] == "terminated" and (
|
|
self.use_coordinator
|
|
or info["tags"][TAG_RAY_NODE_KIND] == node_type
|
|
):
|
|
info["tags"] = tags
|
|
info["state"] = "running"
|
|
info.pop("teardown_complete", None)
|
|
self.state.put(node_id, info)
|
|
count = count - 1
|
|
if count == 0:
|
|
return
|
|
|
|
def terminate_node(self, node_id):
|
|
workers = self.state.get()
|
|
info = workers[node_id]
|
|
info["state"] = "terminated"
|
|
info["teardown_complete"] = True
|
|
self.state.put(node_id, info)
|
|
|
|
@staticmethod
|
|
def bootstrap_config(cluster_config):
|
|
return bootstrap_local(cluster_config)
|
|
|
|
|
|
def record_local_head_state_if_needed(local_provider: LocalNodeProvider) -> None:
|
|
"""This function is called on the Ray head from StandardAutoscaler.reset
|
|
to record the head node's own existence in the cluster state file.
|
|
|
|
This is necessary because `provider.create_node` in
|
|
`commands.get_or_create_head_node` records the head state on the
|
|
cluster-launching machine but not on the head.
|
|
"""
|
|
head_ip = local_provider.provider_config["head_ip"]
|
|
cluster_name = local_provider.cluster_name
|
|
# If the head node is not marked as created in the cluster state file,
|
|
if head_ip not in local_provider.non_terminated_nodes({}):
|
|
# These tags are based on the ones in commands.get_or_create_head_node;
|
|
# keep in sync.
|
|
head_tags = {
|
|
TAG_RAY_NODE_KIND: NODE_KIND_HEAD,
|
|
TAG_RAY_USER_NODE_TYPE: LOCAL_CLUSTER_NODE_TYPE,
|
|
TAG_RAY_NODE_NAME: "ray-{}-head".format(cluster_name),
|
|
TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE,
|
|
}
|
|
# Mark the head node as created in the cluster state file.
|
|
local_provider.create_node(node_config={}, tags=head_tags, count=1)
|
|
|
|
assert head_ip in local_provider.non_terminated_nodes({})
|