fix(lingxing): allow bounded configurable business request pacing

This commit is contained in:
2026-10-13 11:46:57 +08:00
parent 6e9466d0b4
commit 39f914b3b5
4 changed files with 109 additions and 5 deletions
@@ -13,6 +13,40 @@ import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
class LingxingEgressGateTest {
@Test void debugTimingRecordsActualCallAndCompletionWithoutContextOrPayload() {
var logger = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger(LingxingEgressGate.class);
var previous = logger.getLevel();
var appender = new ch.qos.logback.core.read.ListAppender<ch.qos.logback.classic.spi.ILoggingEvent>();
appender.start(); logger.addAppender(appender); logger.setLevel(ch.qos.logback.classic.Level.DEBUG);
try {
var gate = gate();
Object body = new Object();
assertThat(gate.<Object>execute(context("private-attempt"), Operation.OUTBOUND_DETAIL, HASH, handle -> {
handle.startHttp(); clock.advanceMillis(123); return body;
})).isSameAs(body);
assertThat(appender.list).hasSize(1);
assertThat(appender.list.get(0).getFormattedMessage())
.contains("operation=OUTBOUND_DETAIL", "start=" + NOW, "end=" + NOW.plusMillis(123),
"upstreamStatus=200", "errorCode=null", "completionConfirmed=true")
.doesNotContain("demo-account", "private-attempt", "demo-grant", HASH);
ledger.failComplete = true;
var failure = assertThrows(LingxingWmsException.class, () -> gate.execute(context("private-failure"),
Operation.OUTBOUND_DETAIL, HASH, handle -> {
handle.startHttp(); clock.advanceMillis(50); throw rateLimit();
}));
assertThat(failure.code()).isEqualTo("LINGXING_WMS_BUDGET_COMPLETION_UNCONFIRMED");
assertThat(appender.list).hasSize(2);
assertThat(appender.list.get(1).getFormattedMessage())
.contains("upstreamStatus=429", "errorCode=RATE_LIMIT", "completionConfirmed=false")
.doesNotContain("private-failure", "synthetic rate limit", "demo-account", HASH);
logger.setLevel(ch.qos.logback.classic.Level.INFO);
ledger.failComplete = false;
gate().execute(context("debug-disabled"), Operation.WAREHOUSES, HASH, handle -> body);
assertThat(appender.list).hasSize(2);
} finally {
logger.detachAppender(appender); appender.stop(); logger.setLevel(previous);
}
}
static final Instant NOW = Instant.parse("2026-10-12T00:00:00Z");
static final String HASH = "a".repeat(64);
private final MutableClock clock = new MutableClock(NOW);
@@ -159,7 +193,7 @@ class LingxingEgressGateTest {
} finally { release.countDown(); executor.shutdownNow(); }
}
@Test void intervalCannotBeConfiguredBelowFiveSecondsAndPropertiesHideGrant() {
assertThatThrownBy(() -> new LingxingEgressProperties(null, null, null, null, null, null, Duration.ofMillis(4999), null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new LingxingEgressProperties(CollectionMode.VALIDATION, null, null, null, null, null, Duration.ofMillis(4999), null)).isInstanceOf(IllegalArgumentException.class);
assertThat(props(CollectionMode.VALIDATION, "test").toString()).doesNotContain("demo-grant", "demo-account", "127.0.0.1");
}
@Test void knownBudgetDenialDoesNotPermanentlyPauseOrConsumeHttp() {