KeepAlive im Verbindungspool
Beide Pool-Clients können ihre Leerlaufverbindungen in einem festen Intervall mit dem Serverjob krn.CheckServerConnection anpingen. Der Job ist serverseitig nur ein Zähler, der über die bestehende Session läuft — das billigste verfügbare Lebenszeichen. Er hält NAT-, Firewall- und Idle-Timeouts offen und deckt still weggeräumte Sockets auf, weil die Schreib- oder Leseoperation dann fehlschlägt.
Ohne KeepAlive fällt eine tote Verbindung erst beim nächsten fachlichen Aufruf auf: dieser Aufruf schlägt fehl, der Pool wirft die Verbindung weg, und erst der Aufruf danach läuft wieder. Genau das verhindert das KeepAlive.
1. Konfiguration
from ecmind_blue_client.pool import SyncPoolClient
pool = SyncPoolClient(
servers="server1:4000:1",
username="<user>",
password="<pass>",
keepalive_interval=300, # Sekunden
)
|
KeepAlive deaktiviert, es wird kein Hintergrund-Worker gestartet |
|
ebenfalls deaktiviert |
|
Sekunden zwischen zwei Durchläufen |
AsyncPoolClient nimmt denselben Parameter entgegen und verwendet statt eines Threads einen asyncio.Task.
Das Intervall richtet sich nach dem kürzesten Idle-Timeout auf dem Weg zum Server (Firewall, Loadbalancer, NAT-Gateway) und sollte deutlich darunter liegen. 300 Sekunden sind ein brauchbarer Startwert.
2. Ablauf eines Durchlaufs
-
Der Worker startet verzögert: erst wenn die erste Verbindung erzeugt wird. Ein Pool, der nie benutzt wird, erzeugt keinen Thread und keinen Task.
-
Das Intervall wird zwischen dem Ende eines Durchlaufs und dem Beginn des nächsten gemessen. Ein langsamer Durchlauf führt daher weder zu Aufholbursts noch zu überlappenden Durchläufen.
-
Pro Durchlauf wird die Anzahl der aktuell freien Verbindungen als Momentaufnahme genommen; genau so viele Verbindungen werden geprüft. Eine geprüfte und zurückgelegte Verbindung kann im selben Durchlauf nicht erneut geprüft werden.
-
Jede entnommene Verbindung wird mit
krn.CheckServerConnection(Flags=0) geprüft:Erfolg
Verbindung geht zurück in den Pool,
keepalive_countundlast_keepalive_atwerden hochgezähltFehler (Returncode
!= 0oder Netzwerkfehler)Verbindung wird geschlossen und aus der Buchführung entfernt
3. Eigenschaften
- Nur Leerlaufverbindungen
-
Eine gerade ausgeliehene Verbindung ist aus der Warteschlange heraus und für den Durchlauf unsichtbar. Laufende Jobs werden nie gestört.
- Keine Sperre über den RPC-Aufruf
-
Nur die Warteschlangen-Buchführung läuft unter dem Pool-Lock, der Serverjob selbst nicht. Ein hängender Server blockiert den Pool also nicht für die Dauer des Durchlaufs.
- Kein Neuaufbau im Durchlauf
-
Eine kaputte Verbindung wird nur geschlossen, nicht ersetzt. Der Pool schrumpft, statt kaputte Slots zu blockieren; der nächste Aufruf erzeugt bei Bedarf eine frische Verbindung.
- Kein Fehler nach aussen
-
Ausfälle landen ausschliesslich auf
logging.DEBUGim Loggerecmind_blue_client.pool._sync_pool_clientbzw.…_async_pool_client. Es gibt kein Event, keinen Callback und keine Exception im aufrufenden Thread. - Getrennt von der Anwendungsstatistik
-
KeepAlive-Verkehr zählt in
keepalive_count/last_keepalive_at, nicht incall_count/last_call_at.
4. Was das KeepAlive nicht tut
-
Es prüft nicht die konfigurierten, aber gerade nicht verbundenen Server. Dafür gibt es
ecm.check_connections(), das pro Server eine eigene Wegwerf-Verbindung aufbaut. -
Es rührt die geteilte Server-Session nicht an. Ist die serverseitige Session weg (Serverneustart, Session-Timeout,
krn.SessionDrop), erkennt das erst der nächste Verbindungsaufbau:krn.SessionAttachliefert dann ein abweichendesSessionGUID, der veraltete Eintrag wird verworfen und ein vollständiger Login durchgeführt. KeepAlive und Session-Recovery sind zwei unabhängige Mechanismen, die sich ergänzen: das KeepAlive entfernt die tote Verbindung, der nächsteexecute()baut eine neue mit frischer Session auf.
5. Statistik auslesen
for stats in pool.connection_stats():
print(
f"{stats.hostname}:{stats.port} "
f"calls={stats.call_count} "
f"keepalives={stats.keepalive_count} "
f"last_keepalive={stats.last_keepalive_at}"
)
6. Pool sauber beenden
Solange ein KeepAlive läuft, sollte der Pool explizit geschlossen werden. close() (sync) bzw. aclose() (async) stoppt den Worker und schliesst alle Leerlaufverbindungen; beide sind mehrfach aufrufbar.
# Sync
pool = SyncPoolClient(..., keepalive_interval=300)
try:
...
finally:
pool.close()
# oder als Kontextmanager
with SyncPoolClient(..., keepalive_interval=300) as pool:
...
# Async
async with AsyncPoolClient(..., keepalive_interval=300) as pool:
...
Der Worker hält den Pool nur über eine schwache Referenz. Ein Pool, der ohne close() einfach losgelassen wird, kann daher weiterhin vom Garbage Collector eingesammelt werden — der Worker beendet sich dann von selbst. Das ist die Rückfallebene, nicht der empfohlene Weg: Zeitpunkt und Reihenfolge bestimmt dann der Garbage Collector.
7. FastAPI
Im Lifespan gehört der Pool einmal aufgebaut und beim Herunterfahren explizit geschlossen:
@asynccontextmanager
async def lifespan(app: FastAPI):
client = AsyncPoolClient(
servers=os.environ["ECM_SERVERS"],
username=os.environ["ECM_USERNAME"],
password=os.environ["ECM_PASSWORD"],
keepalive_interval=300,
)
app.state.ecm = ECM(client)
yield
await client.aclose()
app.state.ecm = None
Wird nur die ECM-Instanz herumgereicht, kommt man über ecm.client an den Pool: await ecm.client.aclose().
Zwei Punkte, die in einem Web-Service leicht übersehen werden:
-
Der
asyncio.Taskwird beim Erzeugen der ersten Verbindung im laufenden Event Loop angelegt, nicht im Konstruktor. Ein im Lifespan gebauter Pool bindet sich damit immer an den Loop, der ihn auch benutzt. -
Läuft der Service mit mehreren Worker-Prozessen (
uvicorn --workers, Gunicorn), hat jeder Prozess seinen eigenen Pool und damit seinen eigenen KeepAlive-Worker. Das Intervall gilt pro Prozess.
Das vollständige Beispiel steht in FastAPI-Integration.