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:
@@ -62,6 +62,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
args = _parse_args(argv)
|
args = _parse_args(argv)
|
||||||
|
|
||||||
config = load_config(args.config) if args.config else Config()
|
config = load_config(args.config) if args.config else Config()
|
||||||
|
if args.config:
|
||||||
|
config.config_file = args.config
|
||||||
|
|
||||||
if args.listen:
|
if args.listen:
|
||||||
if ":" in args.listen:
|
if ":" in args.listen:
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class Config:
|
|||||||
log_level: str = "info"
|
log_level: str = "info"
|
||||||
proxy_source: ProxySourceConfig | None = None
|
proxy_source: ProxySourceConfig | None = None
|
||||||
proxy_pool: ProxyPoolConfig | None = None
|
proxy_pool: ProxyPoolConfig | None = None
|
||||||
|
config_file: str = ""
|
||||||
|
|
||||||
|
|
||||||
def parse_proxy_url(url: str) -> ChainHop:
|
def parse_proxy_url(url: str) -> ChainHop:
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ class ProxyPool:
|
|||||||
self._tasks.append(asyncio.create_task(self._refresh_loop()))
|
self._tasks.append(asyncio.create_task(self._refresh_loop()))
|
||||||
self._tasks.append(asyncio.create_task(self._health_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:
|
async def stop(self) -> None:
|
||||||
"""Cancel background tasks and save state."""
|
"""Cancel background tasks and save state."""
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import signal
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config, load_config
|
||||||
from .metrics import Metrics
|
from .metrics import Metrics
|
||||||
from .pool import ProxyPool
|
from .pool import ProxyPool
|
||||||
from .proto import ProtoError, Socks5Reply, build_chain, read_socks5_address
|
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):
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||||
loop.add_signal_handler(sig, lambda s=sig: stop.set_result(s))
|
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()
|
metrics_stop = asyncio.Event()
|
||||||
pool_ref = proxy_pool if isinstance(proxy_pool, ProxyPool) else None
|
pool_ref = proxy_pool if isinstance(proxy_pool, ProxyPool) else None
|
||||||
metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))
|
metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))
|
||||||
|
|||||||
Reference in New Issue
Block a user