-
Notifications
You must be signed in to change notification settings - Fork 21
Add automatic Yuanrong startup to interface.py #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,10 @@ | |||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||
| import socket | ||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||
| import tempfile | ||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||
| from importlib import resources | ||||||||||||||||||||||||
| from typing import Any, Optional | ||||||||||||||||||||||||
|
|
@@ -103,11 +106,11 @@ def _maybe_create_transferqueue_storage(conf: DictConfig) -> DictConfig: | |||||||||||||||||||||||
| check = subprocess.run(["pgrep", "-f", "mooncake_master"], stdout=subprocess.PIPE, text=True) | ||||||||||||||||||||||||
| if check.returncode == 0: | ||||||||||||||||||||||||
| pids = check.stdout.strip().replace("\n", ", ") | ||||||||||||||||||||||||
| logging.info(f"Find existing mooncake_master (PID: {pids}), try to kill first...") | ||||||||||||||||||||||||
| logger.info(f"Find existing mooncake_master (PID: {pids}), try to kill first...") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| result = os.system('pkill -f "[m]ooncake_master"') | ||||||||||||||||||||||||
| if result == 0: | ||||||||||||||||||||||||
| logging.info("Successfully killed existing mooncake_master processes.") | ||||||||||||||||||||||||
| logger.info("Successfully killed existing mooncake_master processes.") | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| raise RuntimeError(f"Failed to kill existing mooncake_master processes (exit code: {result}).") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -185,6 +188,129 @@ def _maybe_create_transferqueue_storage(conf: DictConfig) -> DictConfig: | |||||||||||||||||||||||
| f"Output:\n{error_msg}" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| _TRANSFER_QUEUE_STORAGE["MooncakeStore"] = process | ||||||||||||||||||||||||
| if conf.backend.storage_backend == "Yuanrong": | ||||||||||||||||||||||||
| if conf.backend.Yuanrong.auto_init: | ||||||||||||||||||||||||
| etcd_process = None | ||||||||||||||||||||||||
| etcd_data_dir = None | ||||||||||||||||||||||||
| worker_address = None | ||||||||||||||||||||||||
| if not shutil.which("etcd"): | ||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||
| "etcd executable not found in PATH. Please install etcd and make sure it's in the PATH." | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if not shutil.which("dscli"): | ||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||
| "dscli executable not found in PATH. Please run `pip install openyuanrong-datasystem`." | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| # ========== Start etcd ========== | ||||||||||||||||||||||||
| etcd_address = "127.0.0.1:2379" | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| etcd_address = conf.backend.Yuanrong.etcd_address | ||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Assume host:port format | ||||||||||||||||||||||||
| parts = etcd_address.split(":") | ||||||||||||||||||||||||
| if len(parts) != 2: | ||||||||||||||||||||||||
| raise ValueError(f"Invalid etcd_address format: {etcd_address}. Expected host:port") | ||||||||||||||||||||||||
| host = parts[0] | ||||||||||||||||||||||||
| port = int(parts[1]) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Create temporary data directory | ||||||||||||||||||||||||
| etcd_data_dir = tempfile.mkdtemp(prefix="tq_etcd_") | ||||||||||||||||||||||||
| logger.info(f"Starting etcd with data directory: {etcd_data_dir}") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| cmd = [ | ||||||||||||||||||||||||
| "etcd", | ||||||||||||||||||||||||
| f"--data-dir={etcd_data_dir}", | ||||||||||||||||||||||||
| f"--listen-client-urls=http://{host}:{port}", | ||||||||||||||||||||||||
| f"--advertise-client-urls=http://{host}:{port}", | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| etcd_process = subprocess.Popen( | ||||||||||||||||||||||||
| cmd, | ||||||||||||||||||||||||
| stdout=subprocess.DEVNULL, | ||||||||||||||||||||||||
| stderr=subprocess.DEVNULL, | ||||||||||||||||||||||||
| text=True, | ||||||||||||||||||||||||
| bufsize=1, | ||||||||||||||||||||||||
| universal_newlines=True, | ||||||||||||||||||||||||
| start_new_session=True, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| time.sleep(3) # Wait for etcd to start | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if etcd_process.poll() is None: | ||||||||||||||||||||||||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||||||||||||||||||||
| sock.settimeout(2) | ||||||||||||||||||||||||
| result = sock.connect_ex((host, port)) | ||||||||||||||||||||||||
| sock.close() | ||||||||||||||||||||||||
| if result != 0: | ||||||||||||||||||||||||
| raise RuntimeError(f"etcd process started but not listening on {host}:{port}") | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| raise RuntimeError(f"etcd exited immediately with return code {etcd_process.returncode}") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| logger.info(f"etcd started, PID: {etcd_process.pid}") | ||||||||||||||||||||||||
| time.sleep(2) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # ========== Start datasystem worker ========== | ||||||||||||||||||||||||
| # Assume host:port format | ||||||||||||||||||||||||
| worker_host = conf.backend.Yuanrong.host | ||||||||||||||||||||||||
| worker_port = conf.backend.Yuanrong.port | ||||||||||||||||||||||||
| worker_address = worker_host + ":" + str(worker_port) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| cmd = [ | ||||||||||||||||||||||||
| "dscli", | ||||||||||||||||||||||||
| "start", | ||||||||||||||||||||||||
| "-w", | ||||||||||||||||||||||||
| "--worker_address", | ||||||||||||||||||||||||
| worker_address, | ||||||||||||||||||||||||
| "--etcd_address", | ||||||||||||||||||||||||
| etcd_address, | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| ds_result = subprocess.run( | ||||||||||||||||||||||||
| cmd, | ||||||||||||||||||||||||
| stdout=subprocess.PIPE, | ||||||||||||||||||||||||
| stderr=subprocess.STDOUT, | ||||||||||||||||||||||||
| text=True, | ||||||||||||||||||||||||
| timeout=90, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| except subprocess.TimeoutExpired as err: | ||||||||||||||||||||||||
| raise RuntimeError(f"dscli start timed out: {err}") from err | ||||||||||||||||||||||||
| # Wait for dscli to start and exit (it starts worker and exits) | ||||||||||||||||||||||||
| if ds_result.returncode == 0 and "[ OK ]" in ds_result.stdout: | ||||||||||||||||||||||||
| logger.info(f"dscli started Yuanrong datasystem worker at {worker_address} successfully.") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||
| f"Failed to start datasystem worker at {worker_address}. " | ||||||||||||||||||||||||
| f"Return code: {ds_result.returncode}, Output: {ds_result.stdout}" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Store processes and data directory | ||||||||||||||||||||||||
| _TRANSFER_QUEUE_STORAGE["Yuanrong"] = { | ||||||||||||||||||||||||
| "etcd": etcd_process, | ||||||||||||||||||||||||
| "etcd_data_dir": etcd_data_dir, | ||||||||||||||||||||||||
| "worker_address": worker_address, | ||||||||||||||||||||||||
| "etcd_address": etcd_address, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| logger.info("Yuanrong backend (etcd + datasystem) started successfully.") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||
| # Clean up on failure | ||||||||||||||||||||||||
| if etcd_process is not None and etcd_process.poll() is None: | ||||||||||||||||||||||||
| etcd_process.terminate() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| etcd_process.terminate() | |
| try: | |
| etcd_process.terminate() | |
| try: | |
| etcd_process.wait(timeout=5) | |
| except subprocess.TimeoutExpired: | |
| etcd_process.kill() | |
| etcd_process.wait() | |
| except Exception: | |
| # Best-effort cleanup; ignore failures here | |
| pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only close etcd when enable auto_init?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. If users start etcd manually, they should close etcd manually.
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If etcd_process.kill() is used after a timeout, the code should still call wait() to reap the child process; otherwise a zombie process can be left behind until interpreter exit. After kill(), call etcd_process.wait() (possibly in a nested try/except) to ensure the process is fully collected.
| etcd_process.kill() | |
| etcd_process.kill() | |
| try: | |
| # Ensure the killed process is fully reaped to avoid zombies | |
| etcd_process.wait(timeout=5) | |
| except subprocess.TimeoutExpired: | |
| logger.warning("Timed out while waiting for etcd process to exit after kill.") | |
| except Exception as e: | |
| logger.warning(f"Error while waiting for etcd process to exit after kill: {e}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add etcd_process.wait()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new Yuanrong auto-init path (
etcd+dscli) introduces substantial side effects duringinit()but currently has no targeted test coverage. Consider adding a unit test that monkeypatchesshutil.which/subprocess.Popen/subprocess.runto validate the happy path and failure cleanup behavior (without requiring realetcd/dsclibinaries).