feat(client): re-resolve the server hostnames at the DNS TTL
hbc, hbc_mini.py and hbc_mini.c resolved the server once at startup, so an address change needed a client restart. They now re-resolve every TTL seconds (smallest A/AAAA TTL, floor 30s, 300s when unknown) and re-point, add or drop connections to follow the new addresses; a failed lookup keeps the current ones. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from .. import __version__
|
||||
|
||||
# Import plugin system
|
||||
from .plugin import PluginRegistry, PluginLoader, InfoPlugin, MonitorPlugin
|
||||
from . import dnsttl
|
||||
from .connectivity import Connectivity, set_shared
|
||||
|
||||
# Constants
|
||||
@@ -95,6 +96,14 @@ class AsyncConnection:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
self.protocol = None
|
||||
|
||||
def set_target(self, addr: str, af: int):
|
||||
"""Re-point at a new server address; the socket is reopened on the next send if the family changed."""
|
||||
if af != self.af:
|
||||
self.close()
|
||||
self.af = af
|
||||
self.addr = addr
|
||||
self.logger = logging.getLogger(f"hbc.conn.{addr}")
|
||||
|
||||
async def sendto(self, msg: dict, msg_id: str = "HTB"):
|
||||
"""Send a message to the server.
|
||||
@@ -500,6 +509,48 @@ def stop():
|
||||
task.cancel()
|
||||
|
||||
|
||||
async def dns_refresher(connections: List[AsyncConnection], hosts: List[str], port: int, af_filter: int,
|
||||
name: str, interval: int, connectivity: Optional[Connectivity], next_id: int):
|
||||
"""Re-resolve the server hosts every TTL seconds and follow address changes.
|
||||
|
||||
A failed lookup leaves the connections untouched. Addresses that disappear are re-pointed at
|
||||
new ones where possible; the rest are retired, and surplus new ones get a connection.
|
||||
"""
|
||||
logger = logging.getLogger("hbc.dns")
|
||||
loop = asyncio.get_running_loop()
|
||||
delay = await loop.run_in_executor(None, _ttl_delay, hosts, af_filter)
|
||||
while running:
|
||||
try:
|
||||
if shutdown_event:
|
||||
await asyncio.wait_for(shutdown_event.wait(), timeout=delay)
|
||||
break
|
||||
await asyncio.sleep(delay)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
resolved = await loop.run_in_executor(None, dnsttl.resolve, hosts, port, af_filter)
|
||||
if resolved is None:
|
||||
delay = dnsttl.FAILED_RETRY
|
||||
continue
|
||||
new, gone = dnsttl.retarget(connections, resolved)
|
||||
for conn in gone:
|
||||
if len(connections) > 1:
|
||||
logger.info(f"Dropping server address {conn.addr}")
|
||||
conn._dead = True
|
||||
conn.close()
|
||||
connections.remove(conn)
|
||||
for af, addr in new:
|
||||
conn = AsyncConnection(next_id, addr, port, af, name)
|
||||
next_id += 1
|
||||
logger.info(f"Adding server address {addr}")
|
||||
connections.append(conn)
|
||||
active_tasks.append(asyncio.create_task(heartbeat_sender(conn, interval, connectivity)))
|
||||
delay = await loop.run_in_executor(None, _ttl_delay, hosts, af_filter)
|
||||
|
||||
|
||||
def _ttl_delay(hosts: List[str], af_filter: int) -> int:
|
||||
return dnsttl.refresh_delay([dnsttl.lookup_ttl(h, af_filter) for h in hosts])
|
||||
|
||||
|
||||
async def cleanup(connections: List[AsyncConnection]):
|
||||
"""Cleanup connections on shutdown."""
|
||||
logger = logging.getLogger("hbc.cleanup")
|
||||
@@ -640,6 +691,9 @@ async def async_main(args, config):
|
||||
task = asyncio.create_task(heartbeat_sender(conn, interval, connectivity))
|
||||
active_tasks.append(task)
|
||||
|
||||
active_tasks.append(asyncio.create_task(
|
||||
dns_refresher(connections, hb_hosts, hb_port, af_filter, iam, interval, connectivity, conn_id)))
|
||||
|
||||
# Plugin collector (uses all connections, but we'll use first one)
|
||||
if connections and registry.get_enabled():
|
||||
task = asyncio.create_task(plugin_collector(connections[0], registry))
|
||||
|
||||
Reference in New Issue
Block a user