feat: add SIGHUP hot config reload

On SIGHUP, re-read the YAML config file and update mutable runtime
settings: timeout, retries, log_level, and pool config (sources,
intervals, thresholds). Pool triggers an immediate source re-fetch.
Listen address and chain require restart.
This commit is contained in:
user
2026-02-15 16:02:57 +01:00
parent fc1dea70f4
commit 818169758b
4 changed files with 37 additions and 1 deletions

View File

@@ -62,6 +62,8 @@ def main(argv: list[str] | None = None) -> int:
args = _parse_args(argv)
config = load_config(args.config) if args.config else Config()
if args.config:
config.config_file = args.config
if args.listen:
if ":" in args.listen:

View File

@@ -74,6 +74,7 @@ class Config:
log_level: str = "info"
proxy_source: ProxySourceConfig | None = None
proxy_pool: ProxyPoolConfig | None = None
config_file: str = ""
def parse_proxy_url(url: str) -> ChainHop:

View File

@@ -90,6 +90,13 @@ class ProxyPool:
self._tasks.append(asyncio.create_task(self._refresh_loop()))
self._tasks.append(asyncio.create_task(self._health_loop()))
async def reload(self, cfg: ProxyPoolConfig) -> None:
"""Update pool config and trigger source re-fetch."""
self._cfg = cfg
logger.info("pool: config reloaded, re-fetching sources")
await self._fetch_all_sources()
self._save_state()
async def stop(self) -> None:
"""Cancel background tasks and save state."""
self._stop.set()

View File

@@ -8,7 +8,7 @@ import signal
import struct
import time
from .config import Config
from .config import Config, load_config
from .metrics import Metrics
from .pool import ProxyPool
from .proto import ProtoError, Socks5Reply, build_chain, read_socks5_address
@@ -253,6 +253,32 @@ async def serve(config: Config) -> None:
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda s=sig: stop.set_result(s))
# SIGHUP: hot-reload config (timeout, retries, log_level, pool settings)
async def _reload() -> None:
if not config.config_file:
logger.warning("reload: no config file specified, ignoring SIGHUP")
return
try:
new = load_config(config.config_file)
except Exception as e:
logger.warning("reload: failed to read config: %s", e)
return
config.timeout = new.timeout
config.retries = new.retries
if new.log_level != config.log_level:
config.log_level = new.log_level
logging.getLogger("s5p").setLevel(
getattr(logging, new.log_level.upper(), logging.INFO),
)
if isinstance(proxy_pool, ProxyPool) and new.proxy_pool:
await proxy_pool.reload(new.proxy_pool)
logger.info("reload: config reloaded")
def _on_sighup() -> None:
asyncio.ensure_future(_reload())
loop.add_signal_handler(signal.SIGHUP, _on_sighup)
metrics_stop = asyncio.Event()
pool_ref = proxy_pool if isinstance(proxy_pool, ProxyPool) else None
metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))