|
16 | 16 | import logging |
17 | 17 | import math |
18 | 18 | import os |
| 19 | +import shutil |
| 20 | +import socket |
19 | 21 | import subprocess |
| 22 | +import tempfile |
20 | 23 | import time |
21 | 24 | from importlib import resources |
22 | 25 | from typing import Any, Optional |
@@ -102,11 +105,11 @@ def _maybe_create_transferqueue_storage(conf: DictConfig) -> DictConfig: |
102 | 105 | check = subprocess.run(["pgrep", "-f", "mooncake_master"], stdout=subprocess.PIPE, text=True) |
103 | 106 | if check.returncode == 0: |
104 | 107 | pids = check.stdout.strip().replace("\n", ", ") |
105 | | - logging.info(f"Find existing mooncake_master (PID: {pids}), try to kill first...") |
| 108 | + logger.info(f"Find existing mooncake_master (PID: {pids}), try to kill first...") |
106 | 109 |
|
107 | 110 | result = os.system('pkill -f "[m]ooncake_master"') |
108 | 111 | if result == 0: |
109 | | - logging.info("Successfully killed existing mooncake_master processes.") |
| 112 | + logger.info("Successfully killed existing mooncake_master processes.") |
110 | 113 | else: |
111 | 114 | raise RuntimeError(f"Failed to kill existing mooncake_master processes (exit code: {result}).") |
112 | 115 |
|
@@ -167,6 +170,193 @@ def _maybe_create_transferqueue_storage(conf: DictConfig) -> DictConfig: |
167 | 170 | f"Output:\n{error_msg}" |
168 | 171 | ) |
169 | 172 | _TRANSFER_QUEUE_STORAGE["MooncakeStore"] = process |
| 173 | + if conf.backend.storage_backend == "Yuanrong": |
| 174 | + if conf.backend.Yuanrong.auto_init: |
| 175 | + etcd_process = None |
| 176 | + etcd_data_dir = None |
| 177 | + |
| 178 | + try: |
| 179 | + # ========== Start etcd (inlined from _start_etcd) ========== |
| 180 | + etcd_address = conf.backend.Yuanrong.etcd_address |
| 181 | + # Parse host and port |
| 182 | + if "://" in etcd_address: |
| 183 | + # Remove protocol prefix if present |
| 184 | + parsed = urlparse(etcd_address) |
| 185 | + host = parsed.hostname |
| 186 | + port = parsed.port |
| 187 | + else: |
| 188 | + # Assume host:port format |
| 189 | + parts = etcd_address.split(":") |
| 190 | + if len(parts) != 2: |
| 191 | + raise ValueError(f"Invalid etcd_address format: {etcd_address}. Expected host:port") |
| 192 | + host = parts[0] |
| 193 | + port = int(parts[1]) |
| 194 | + |
| 195 | + # Check if etcd is already running |
| 196 | + check = subprocess.run(["pgrep", "-f", "etcd"], stdout=subprocess.PIPE, text=True) |
| 197 | + if check.returncode == 0: |
| 198 | + pids = check.stdout.strip().replace("\n", ", ") |
| 199 | + logger.warning(f"Found existing etcd processes (PID: {pids}). TQ will not start a new one.") |
| 200 | + # Try to connect to see if it's listening on our desired port |
| 201 | + try: |
| 202 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 203 | + sock.settimeout(2) |
| 204 | + result = sock.connect_ex((host, port)) |
| 205 | + sock.close() |
| 206 | + if result == 0: |
| 207 | + logger.info(f"etcd is already listening on {host}:{port}") |
| 208 | + etcd_process = None |
| 209 | + etcd_data_dir = None |
| 210 | + else: |
| 211 | + logger.warning( |
| 212 | + f"etcd process exists but not listening on {host}:{port}, will start new instance" |
| 213 | + ) |
| 214 | + # Continue to start new instance |
| 215 | + except Exception as e: |
| 216 | + logger.warning(f"Failed to check etcd port: {e}, will start new instance") |
| 217 | + # Continue to start new instance |
| 218 | + |
| 219 | + if etcd_process is None: |
| 220 | + # Create temporary data directory |
| 221 | + etcd_data_dir = tempfile.mkdtemp(prefix="tq_etcd_") |
| 222 | + logger.info(f"Starting etcd with data directory: {etcd_data_dir}") |
| 223 | + |
| 224 | + cmd = [ |
| 225 | + "etcd", |
| 226 | + f"--data-dir={etcd_data_dir}", |
| 227 | + f"--listen-client-urls=http://0.0.0.0:{port}", |
| 228 | + f"--advertise-client-urls=http://{host}:{port}", |
| 229 | + "--listen-peer-urls=http://0.0.0.0:2380", |
| 230 | + f"--initial-advertise-peer-urls=http://{host}:2380", |
| 231 | + "--initial-cluster=default=http://{}:2380".format(host), |
| 232 | + ] |
| 233 | + |
| 234 | + log_file_path = "/tmp/etcd.log" |
| 235 | + with open(log_file_path, "w") as log_file: |
| 236 | + etcd_process = subprocess.Popen( |
| 237 | + cmd, |
| 238 | + stdout=log_file, |
| 239 | + stderr=subprocess.STDOUT, |
| 240 | + text=True, |
| 241 | + bufsize=1, |
| 242 | + universal_newlines=True, |
| 243 | + start_new_session=True, |
| 244 | + ) |
| 245 | + time.sleep(3) # Wait for etcd to start |
| 246 | + |
| 247 | + if etcd_process.poll() is None: |
| 248 | + logger.info( |
| 249 | + f"etcd started, PID: {etcd_process.pid}. Logs at: {os.path.abspath(log_file_path)}" |
| 250 | + ) |
| 251 | + else: |
| 252 | + error_msg = "" |
| 253 | + try: |
| 254 | + with open(log_file_path) as f: |
| 255 | + error_msg = f.read() |
| 256 | + except Exception as e: |
| 257 | + error_msg = f"Failed to read log file: {e}" |
| 258 | + |
| 259 | + # Clean up data directory on failure |
| 260 | + try: |
| 261 | + shutil.rmtree(etcd_data_dir, ignore_errors=True) |
| 262 | + except Exception: |
| 263 | + pass |
| 264 | + |
| 265 | + raise RuntimeError( |
| 266 | + f"etcd exited with error. Check {log_file_path} for detailed logs. Output:\n{error_msg}" |
| 267 | + ) |
| 268 | + |
| 269 | + # Wait a moment for etcd to be ready if we started it |
| 270 | + if etcd_process is not None: |
| 271 | + time.sleep(2) |
| 272 | + |
| 273 | + # ========== Start datasystem worker (inlined from _start_dscli) ========== |
| 274 | + worker_address = conf.backend.Yuanrong.worker_address |
| 275 | + # Check if datasystem worker is already running (by checking worker port) |
| 276 | + parts = worker_address.split(":") |
| 277 | + if len(parts) != 2: |
| 278 | + raise ValueError(f"Invalid worker_address format: {worker_address}. Expected host:port") |
| 279 | + worker_host = parts[0] |
| 280 | + worker_port = int(parts[1]) |
| 281 | + |
| 282 | + try: |
| 283 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 284 | + sock.settimeout(2) |
| 285 | + result = sock.connect_ex((worker_host, worker_port)) |
| 286 | + sock.close() |
| 287 | + if result == 0: |
| 288 | + logger.info(f"Datasystem worker already listening on {worker_address}") |
| 289 | + else: |
| 290 | + # Start datasystem worker |
| 291 | + cmd = [ |
| 292 | + "dscli", |
| 293 | + "start", |
| 294 | + "-w", |
| 295 | + f"--worker_address={worker_address}", |
| 296 | + f"--etcd_address={etcd_address}", |
| 297 | + ] |
| 298 | + |
| 299 | + log_file_path = "/tmp/dscli.log" |
| 300 | + with open(log_file_path, "w") as log_file: |
| 301 | + process = subprocess.Popen( |
| 302 | + cmd, |
| 303 | + stdout=log_file, |
| 304 | + stderr=subprocess.STDOUT, |
| 305 | + text=True, |
| 306 | + bufsize=1, |
| 307 | + universal_newlines=True, |
| 308 | + start_new_session=True, |
| 309 | + ) |
| 310 | + # Wait for dscli to start and exit (it starts worker and exits) |
| 311 | + time.sleep(3) |
| 312 | + |
| 313 | + if process.poll() is not None: |
| 314 | + # dscli exited, check if it succeeded by verifying port |
| 315 | + time.sleep(2) # Give worker time to start |
| 316 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 317 | + sock.settimeout(2) |
| 318 | + result = sock.connect_ex((worker_host, worker_port)) |
| 319 | + sock.close() |
| 320 | + if result != 0: |
| 321 | + error_msg = "" |
| 322 | + try: |
| 323 | + with open(log_file_path) as f: |
| 324 | + error_msg = f.read() |
| 325 | + except Exception as e: |
| 326 | + error_msg = f"Failed to read log file: {e}" |
| 327 | + raise RuntimeError( |
| 328 | + f"Failed to start datasystem worker. Check {log_file_path} for logs. " |
| 329 | + f"Output:\n{error_msg}" |
| 330 | + ) |
| 331 | + else: |
| 332 | + logger.info(f"Datasystem worker started and listening on {worker_address}") |
| 333 | + else: |
| 334 | + # dscli is still running (unexpected), log warning |
| 335 | + logger.warning( |
| 336 | + f"dscli process still running (PID: {process.pid}). This may indicate an issue." |
| 337 | + ) |
| 338 | + except Exception as e: |
| 339 | + raise RuntimeError(f"Failed to start datasystem worker: {e}") from e |
| 340 | + |
| 341 | + # Store processes and data directory |
| 342 | + _TRANSFER_QUEUE_STORAGE["Yuanrong"] = { |
| 343 | + "etcd": etcd_process, |
| 344 | + "etcd_data_dir": etcd_data_dir, |
| 345 | + "worker_address": worker_address, |
| 346 | + "etcd_address": etcd_address, |
| 347 | + } |
| 348 | + logger.info("Yuanrong backend (etcd + datasystem) started successfully.") |
| 349 | + |
| 350 | + except Exception as e: |
| 351 | + # Clean up on failure |
| 352 | + if etcd_process is not None and etcd_process.poll() is None: |
| 353 | + etcd_process.terminate() |
| 354 | + if etcd_data_dir is not None: |
| 355 | + try: |
| 356 | + shutil.rmtree(etcd_data_dir, ignore_errors=True) |
| 357 | + except Exception: |
| 358 | + pass |
| 359 | + raise RuntimeError(f"Failed to start Yuanrong backend: {e}") from e |
170 | 360 | return conf |
171 | 361 |
|
172 | 362 |
|
@@ -328,6 +518,42 @@ def close(): |
328 | 518 | logger.info("Successfully removed all existing keys in mooncake_master.") |
329 | 519 | except Exception: |
330 | 520 | pass |
| 521 | + elif key == "Yuanrong": |
| 522 | + # Stop etcd process and clean up data directory, stop datasystem worker via dscli |
| 523 | + if isinstance(value, dict): |
| 524 | + etcd_process = value.get("etcd") |
| 525 | + etcd_data_dir = value.get("etcd_data_dir") |
| 526 | + worker_address = value.get("worker_address") |
| 527 | + |
| 528 | + # Stop etcd if running |
| 529 | + if etcd_process is not None and etcd_process.poll() is None: |
| 530 | + etcd_process.terminate() |
| 531 | + try: |
| 532 | + etcd_process.wait(timeout=5) |
| 533 | + except subprocess.TimeoutExpired: |
| 534 | + etcd_process.kill() |
| 535 | + |
| 536 | + # Clean up etcd data directory |
| 537 | + if etcd_data_dir is not None and os.path.exists(etcd_data_dir): |
| 538 | + try: |
| 539 | + shutil.rmtree(etcd_data_dir, ignore_errors=True) |
| 540 | + logger.info(f"Cleaned up etcd data directory: {etcd_data_dir}") |
| 541 | + except Exception as e: |
| 542 | + logger.warning(f"Failed to clean up etcd data directory {etcd_data_dir}: {e}") |
| 543 | + |
| 544 | + # Stop datasystem worker via dscli command |
| 545 | + if worker_address: |
| 546 | + try: |
| 547 | + subprocess.run( |
| 548 | + ["dscli", "stop", "--worker_address", worker_address], |
| 549 | + timeout=5, |
| 550 | + capture_output=True, |
| 551 | + ) |
| 552 | + logger.info(f"Stopped datasystem worker at {worker_address} via dscli stop") |
| 553 | + except Exception as e: |
| 554 | + logger.warning(f"Failed to stop datasystem worker via dscli: {e}") |
| 555 | + else: |
| 556 | + logger.warning(f"Unexpected Yuanrong storage value: {value}") |
331 | 557 | else: |
332 | 558 | logger.warning(f"close for _TRANSFER_QUEUE_STORAGE with key {key} is not supported for now.") |
333 | 559 |
|
|
0 commit comments