fix: 修复docker模式下沙盒启动时端口分配问题
This commit is contained in:
parent
590001c130
commit
fe3d5b7f33
|
|
@ -105,7 +105,10 @@ class LocalContainerBackend(SandboxBackend):
|
|||
RuntimeError: If the container fails to start.
|
||||
"""
|
||||
container_name = f"{self._container_prefix}-{sandbox_id}"
|
||||
port = get_free_port(start_port=self._base_port)
|
||||
# Check host-side Docker published ports as well, because this code may
|
||||
# run inside gateway/langgraph containers while creating sibling sandbox
|
||||
# containers via host Docker socket.
|
||||
port = get_free_port(start_port=self._base_port, check_docker_host_ports=True)
|
||||
try:
|
||||
container_id = self._start_container(container_name, port, extra_mounts)
|
||||
self._ensure_user_data_permissions(container_name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""Thread-safe network utilities."""
|
||||
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
|
@ -32,7 +34,33 @@ class PortAllocator:
|
|||
self._lock = threading.Lock()
|
||||
self._reserved_ports: set[int] = set()
|
||||
|
||||
def _is_port_available(self, port: int) -> bool:
|
||||
@staticmethod
|
||||
def _is_docker_host_port_in_use(port: int) -> bool:
|
||||
"""Check whether a host port is already published by Docker containers.
|
||||
|
||||
This is useful when running inside a container (e.g. gateway/langgraph)
|
||||
while creating sibling containers through the host Docker daemon.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "ps", "--format", "{{.Ports}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5,
|
||||
)
|
||||
except Exception:
|
||||
# Fail-open to preserve previous behavior when docker CLI is unavailable.
|
||||
return False
|
||||
|
||||
pattern = re.compile(r":(\d+)->")
|
||||
for line in result.stdout.splitlines():
|
||||
for match in pattern.finditer(line):
|
||||
if int(match.group(1)) == port:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_port_available(self, port: int, *, check_docker_host_ports: bool = False) -> bool:
|
||||
"""Check if a port is available for binding.
|
||||
|
||||
Args:
|
||||
|
|
@ -44,6 +72,9 @@ class PortAllocator:
|
|||
if port in self._reserved_ports:
|
||||
return False
|
||||
|
||||
if check_docker_host_ports and self._is_docker_host_port_in_use(port):
|
||||
return False
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.bind(("localhost", port))
|
||||
|
|
@ -51,7 +82,7 @@ class PortAllocator:
|
|||
except OSError:
|
||||
return False
|
||||
|
||||
def allocate(self, start_port: int = 8080, max_range: int = 100) -> int:
|
||||
def allocate(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
|
||||
"""Allocate an available port in a thread-safe manner.
|
||||
|
||||
This method is thread-safe. It finds an available port, marks it as reserved,
|
||||
|
|
@ -69,7 +100,7 @@ class PortAllocator:
|
|||
"""
|
||||
with self._lock:
|
||||
for port in range(start_port, start_port + max_range):
|
||||
if self._is_port_available(port):
|
||||
if self._is_port_available(port, check_docker_host_ports=check_docker_host_ports):
|
||||
self._reserved_ports.add(port)
|
||||
return port
|
||||
|
||||
|
|
@ -85,7 +116,7 @@ class PortAllocator:
|
|||
self._reserved_ports.discard(port)
|
||||
|
||||
@contextmanager
|
||||
def allocate_context(self, start_port: int = 8080, max_range: int = 100):
|
||||
def allocate_context(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False):
|
||||
"""Context manager for port allocation with automatic release.
|
||||
|
||||
Args:
|
||||
|
|
@ -95,7 +126,7 @@ class PortAllocator:
|
|||
Yields:
|
||||
An available port number.
|
||||
"""
|
||||
port = self.allocate(start_port, max_range)
|
||||
port = self.allocate(start_port, max_range, check_docker_host_ports=check_docker_host_ports)
|
||||
try:
|
||||
yield port
|
||||
finally:
|
||||
|
|
@ -106,7 +137,7 @@ class PortAllocator:
|
|||
_global_port_allocator = PortAllocator()
|
||||
|
||||
|
||||
def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
|
||||
def get_free_port(start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
|
||||
"""Get a free port in a thread-safe manner.
|
||||
|
||||
This function uses a global port allocator to ensure that concurrent calls
|
||||
|
|
@ -123,7 +154,11 @@ def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
|
|||
Raises:
|
||||
RuntimeError: If no available port is found in the specified range.
|
||||
"""
|
||||
return _global_port_allocator.allocate(start_port, max_range)
|
||||
return _global_port_allocator.allocate(
|
||||
start_port,
|
||||
max_range,
|
||||
check_docker_host_ports=check_docker_host_ports,
|
||||
)
|
||||
|
||||
|
||||
def release_port(port: int) -> None:
|
||||
|
|
|
|||
Loading…
Reference in New Issue