diff --git a/bridged/src/main/java/dev/ltms/bridged/Bridged.java b/bridged/src/main/java/dev/ltms/bridged/Bridged.java index 3cecfd0..f2be472 100644 --- a/bridged/src/main/java/dev/ltms/bridged/Bridged.java +++ b/bridged/src/main/java/dev/ltms/bridged/Bridged.java @@ -70,9 +70,6 @@ public final class Bridged { private static final Logger log = LoggerFactory.getLogger(Bridged.class); - /** How often the injector samples a busy worker's status while it has queued work. */ - private static final long INJECT_POLL_MILLIS = 250; - /** CB-504: how long to wait at startup for herdr's socket before serving degraded. */ private static final long HERDR_WAIT_SECONDS = 30; private static final long HERDR_WAIT_POLL_MILLIS = 500; @@ -290,7 +287,7 @@ public final class Bridged { }; Injector injector = new Injector(agents, turnListener, deliverableTo(presence, leads), presence::forget); - StatusPoller poller = new StatusPoller(agents, injector, INJECT_POLL_MILLIS); + StatusPoller poller = new StatusPoller(agents, injector, Injector.POLL_INTERVAL_MILLIS); poller.start(); // CB-307: reply inbox. A broker: block (with a uri) selects the AMQP-backed durable adapter; diff --git a/bridged/src/main/java/dev/ltms/bridged/inject/Injector.java b/bridged/src/main/java/dev/ltms/bridged/inject/Injector.java index f72b902..82f8854 100644 --- a/bridged/src/main/java/dev/ltms/bridged/inject/Injector.java +++ b/bridged/src/main/java/dev/ltms/bridged/inject/Injector.java @@ -78,6 +78,15 @@ public final class Injector { */ private static final int READINESS_GRACE_POLLS = 240; + /** + * The single source for the injector poll cadence — how often the {@link StatusPoller} drives + * {@link #onStatus} at. {@code Bridged} passes this to every {@link StatusPoller} it constructs, + * and this class reads it to state the readiness grace in seconds on the CB-562 expiry log + * instead of hardcoding "60s". One constant, so a cadence change cannot silently desync a log + * that claims a grace duration. + */ + public static final long POLL_INTERVAL_MILLIS = 250; + private final AgentControl agents; private final TurnListener turnListener; private final Predicate ready; // CB-113: a target is deliverable only when available @@ -264,6 +273,11 @@ public final class Injector { // fail every queued message and release the target (CB-114) instead of // polling it indefinitely with the caller's future never completing. notReady = new ArrayList<>(t.queue); + log.warn("readiness grace for {} expired after {} polls ({}s): target never " + + "became deliverable, so failing {} queued message(s) that never " + + "reached its pane", + target, READINESS_GRACE_POLLS, + READINESS_GRACE_POLLS * POLL_INTERVAL_MILLIS / 1000, notReady.size()); t.queue.clear(); t.notReadySincePoll = 0; } diff --git a/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java b/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java index d7e953d..bf276e0 100644 --- a/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java @@ -1,10 +1,15 @@ package dev.ltms.bridged.inject; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.AgentStatus; import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.HerdrException; import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -369,6 +374,40 @@ class InjectorTest { assertTrue(inj.activeTargets().isEmpty(), "the target is reclaimed, not polled forever"); } + @Test + void readinessGraceExpiryIsLogged() { + // CB-562: the grace-expiry path used to clear the queue silently, so a message that never + // reached the worker's pane surfaced elsewhere as an unrelated turn-stall failure. Assert the + // expiry now names the real cause. (ListAppender capture pattern mirrors AuditLogTest.) + LoggerContext ctx = (LoggerContext) LoggerFactory.getILoggerFactory(); + ch.qos.logback.classic.Logger injectorLog = + (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Injector.class); + ListAppender appender = new ListAppender<>(); + appender.setContext(ctx); + appender.start(); + injectorLog.addAppender(appender); + injectorLog.setLevel(Level.WARN); + try { + Injector inj = new Injector(new AgentControl(herdr), TurnListener.NOOP, _ -> false, _ -> { + }); + inj.enqueue(T, "task"); + + for (int i = 0; i < READINESS_SAMPLES; i++) inj.onStatus(T, AgentStatus.IDLE); + + String warn = appender.list.stream() + .filter(e -> e.getLevel().equals(Level.WARN)) + .map(ILoggingEvent::getFormattedMessage) + .findFirst() + .orElse("no grace-expiry WARN logged"); + assertTrue(warn.contains(T), "the log names the target terminal: " + warn); + assertTrue(warn.contains("never reached"), "the log names the real cause: " + warn); + assertTrue(warn.contains("1 queued message"), + "the log carries the failed message count: " + warn); + } finally { + injectorLog.detachAppender(appender); + } + } + @Test void aWorkerThatBecomesReadyWithinTheGraceIsDeliveredNormally() { // The readiness grace must not fail a worker that is merely slow to boot: once it becomes