refactor and rewrite for asyncio
This commit is contained in:
+98
-18
@@ -1,9 +1,9 @@
|
||||
"""DNS update helper and thread for heartbeat daemon."""
|
||||
"""DNS update helper and pure asyncio worker for heartbeat daemon."""
|
||||
from __future__ import annotations
|
||||
import threading
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
|
||||
def create_nsupdate_payload(hostname: str, newip: str, dyndomain: str, dnsttl: str = "5") -> str:
|
||||
@@ -54,38 +54,118 @@ def nsupdate(hostname: str, newip: str, dyndomain: str, nsupdate_bin: str = "/us
|
||||
return out
|
||||
|
||||
|
||||
def dnsupdatethread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None):
|
||||
"""Thread target: process dns update queue from hbdclass.Host.dnsQ.
|
||||
async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None):
|
||||
"""Pure async DNS worker that processes updates from asyncio.Queue.
|
||||
|
||||
hbdclass: module with Host class that exposes dnsQ queue
|
||||
cfg: configuration mapping with 'dyndomains' and 'nsupdate_bin'
|
||||
log: callable(host, message)
|
||||
email: callable(subject, message)
|
||||
Exits when it receives a None sentinel.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
dnsq = async_queue
|
||||
if not dnsq:
|
||||
if log:
|
||||
try:
|
||||
await loop.run_in_executor(None, log, None, "dns_update_worker: no queue available")
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
while True:
|
||||
name, addr = hbdclass.Host.dnsQ.get()
|
||||
try:
|
||||
item = await dnsq.get()
|
||||
except Exception as e:
|
||||
if log:
|
||||
try:
|
||||
await loop.run_in_executor(None, log, None, f"dns_update_worker: error getting item: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
|
||||
if item is None:
|
||||
break
|
||||
|
||||
try:
|
||||
name, addr = item
|
||||
except Exception:
|
||||
try:
|
||||
dnsq.task_done()
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
m = f"changed address to {addr}"
|
||||
for dyndomain in cfg.get("dyndomains", []):
|
||||
err = nsupdate(name, addr, dyndomain, nsupdate_bin=cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"))
|
||||
err = await loop.run_in_executor(None, nsupdate, name, addr, dyndomain, cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"), cfg.get("rndc_key", "/etc/dhcpc/rndc-key"))
|
||||
if err:
|
||||
m += f", DNS update failed: {err}"
|
||||
if email:
|
||||
try:
|
||||
email("error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}")
|
||||
await loop.run_in_executor(None, email, "error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
m += ", DNS updated."
|
||||
hbdclass.Host.dnsQ.task_done()
|
||||
|
||||
try:
|
||||
dnsq.task_done()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if log:
|
||||
try:
|
||||
log(name, m)
|
||||
await loop.run_in_executor(None, log, name, m)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if log:
|
||||
try:
|
||||
await loop.run_in_executor(None, log, None, "dns_update_worker exiting")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start_dns_thread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None) -> threading.Thread:
|
||||
t = threading.Thread(target=dnsupdatethread, args=(hbdclass, cfg, log, email))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
return t
|
||||
|
||||
def start_dns_worker(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None):
|
||||
"""Start the async DNS worker and return the Task.
|
||||
|
||||
Replaces Host.dnsQ with an asyncio.Queue wrapped in a thread-safe bridge
|
||||
so legacy synchronous put() calls from UDP handlers still work.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Create asyncio.Queue and wrap in a bridge for thread-safe puts
|
||||
async_q = asyncio.Queue()
|
||||
|
||||
class _QueueBridge:
|
||||
"""Thread-safe wrapper around asyncio.Queue for synchronous callers."""
|
||||
def __init__(self, loop, aq):
|
||||
self._loop = loop
|
||||
self._aq = aq
|
||||
|
||||
def put(self, item):
|
||||
"""Thread-safe put that schedules onto event loop."""
|
||||
try:
|
||||
# Try to detect if we're in the event loop thread
|
||||
asyncio.get_running_loop()
|
||||
# We're in event loop context, use put_nowait directly
|
||||
self._aq.put_nowait(item)
|
||||
except RuntimeError:
|
||||
# We're in a different thread, schedule safely
|
||||
try:
|
||||
self._loop.call_soon_threadsafe(self._aq.put_nowait, item)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def task_done(self):
|
||||
"""Delegate task_done to asyncio.Queue."""
|
||||
try:
|
||||
self._aq.task_done()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bridge = _QueueBridge(loop, async_q)
|
||||
hbdclass.Host.dnsQ = bridge
|
||||
|
||||
task = loop.create_task(dns_update_worker(hbdclass, cfg, async_queue=async_q, log=log, email=email, loop=loop))
|
||||
return task
|
||||
|
||||
Reference in New Issue
Block a user