Gate librarian cog on a full ping round-trip, not a bare GET
The librarian health check was a plain GET to '/', which only proved
Flask was listening - not that the service could actually take a query,
run it through its internal queue+worker, and answer back. So the cog
could load against a librarian whose worker was wedged or that couldn't
reach the bot on the return leg.
Replace it with a ping that travels the SAME path a real search does, on
both sides:
bot: QueryControl -> OUT_COMM_Q -> scan_queue -> awaiting_q
librarian: POST /ping -> librarian_queue -> worker pulls it off
(no Crossref/DOI search) -> pongs back with the same uuid
bot: /conjurer -> incoming_q -> scan_incoming matches uuid, wakes waiter
The cog enables only when that whole loop closes within 3s. This also
proves the librarian->bot return path, which a GET never did.
Safety: uuid is random per ping; the wait and POST are both bounded so
startup can't stall; a pong that finds no waiter is dropped (never
orphaned into IN_COMM_Q, which would make the cog post a bogus 'no
results' message); and a ping whose pong never returns is swept out of
awaiting_q after PING_TTL_SECONDS so nothing leaks. All awaiting_q writes
stay within scan_queue (append) and scan_incoming (remove) - no locks,
no cross-thread mutation.
Integration tests cover: OK round-trip, timeout when accepted-but-no-pong,
unreachable, non-200, orphan-pong-dropped, and that real results still
reach IN_COMM_Q. Suite: 24 integration + 41 unit green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,17 +30,23 @@ import discord
|
||||
import requests
|
||||
from discord.ext import commands
|
||||
|
||||
from communication_subroutine import comm_subroutine
|
||||
from communication_subroutine import comm_subroutine, librarian_ping
|
||||
from constants import (
|
||||
ENCODING,
|
||||
FILE_SERVICE_ADDRESS,
|
||||
GET_MP3,
|
||||
LIBRARIAN_PING,
|
||||
LIBRARIAN_SERVICE_ADDRESS,
|
||||
LOGFILE,
|
||||
RADIO_SERVICE_ADDRESS,
|
||||
TOKEN,
|
||||
service_headers,
|
||||
)
|
||||
|
||||
# Round-trip health check budget for the librarian ping (bot -> librarian
|
||||
# internal queue -> pong back). Deliberately short so startup never stalls.
|
||||
LIBRARIAN_PING_TIMEOUT = 3.0
|
||||
|
||||
logger = logging.getLogger("discord")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
@@ -110,9 +116,11 @@ SERVICE_EXTENSION_GROUPS = {
|
||||
"health_url": f"{RADIO_SERVICE_ADDRESS}/ping",
|
||||
"extensions": ["radio_commands"],
|
||||
},
|
||||
# librarian: DOI / Crossref search
|
||||
# librarian: DOI / Crossref search. NOTE: this one is NOT a plain GET - it
|
||||
# is gated on a full ping round-trip (see _load_service_groups); the URL
|
||||
# here is only the label used in the "unreachable" log line.
|
||||
"librarian": {
|
||||
"health_url": f"{LIBRARIAN_SERVICE_ADDRESS}/",
|
||||
"health_url": f"{LIBRARIAN_SERVICE_ADDRESS}{LIBRARIAN_PING}",
|
||||
"extensions": ["librarian_commands"],
|
||||
},
|
||||
}
|
||||
@@ -154,17 +162,37 @@ async def _load_service_groups() -> bool:
|
||||
missing = [e for e in group["extensions"] if e not in client.extensions]
|
||||
if not missing:
|
||||
continue
|
||||
err = await asyncio.to_thread(_service_health, group["health_url"])
|
||||
if err is not None:
|
||||
logger.warning(
|
||||
"Service '%s' unreachable (%s) [%s: %s] - cogs stay disabled: %s",
|
||||
service,
|
||||
group["health_url"],
|
||||
type(err).__name__,
|
||||
err,
|
||||
", ".join(missing),
|
||||
if service == "librarian":
|
||||
# A plain GET only proves Flask is up. The librarian is only useful
|
||||
# once its internal queue + worker are flowing, so prove exactly that
|
||||
# with a ping that must complete the full round-trip (see
|
||||
# communication_subroutine.librarian_ping).
|
||||
alive = await asyncio.to_thread(
|
||||
librarian_ping,
|
||||
LIBRARIAN_SERVICE_ADDRESS,
|
||||
LIBRARIAN_PING,
|
||||
service_headers(),
|
||||
LIBRARIAN_PING_TIMEOUT,
|
||||
)
|
||||
continue
|
||||
if not alive:
|
||||
logger.warning(
|
||||
"Service 'librarian' ping round-trip failed (%s) - cogs stay disabled: %s",
|
||||
group["health_url"],
|
||||
", ".join(missing),
|
||||
)
|
||||
continue
|
||||
else:
|
||||
err = await asyncio.to_thread(_service_health, group["health_url"])
|
||||
if err is not None:
|
||||
logger.warning(
|
||||
"Service '%s' unreachable (%s) [%s: %s] - cogs stay disabled: %s",
|
||||
service,
|
||||
group["health_url"],
|
||||
type(err).__name__,
|
||||
err,
|
||||
", ".join(missing),
|
||||
)
|
||||
continue
|
||||
logger.info("Service '%s' is alive - enabling: %s", service, ", ".join(missing))
|
||||
for extension in missing:
|
||||
if await _load_extension_safe(extension):
|
||||
|
||||
Reference in New Issue
Block a user