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
@@ -18,9 +18,10 @@ public record LingxingEgressProperties(@DefaultValue("OFF") CollectionMode colle
environmentLabel = environmentLabel == null ? "test" : environmentLabel;
minStartInterval = minStartInterval == null ? Duration.ofSeconds(5) : minStartInterval;
cooldownFallback = cooldownFallback == null ? Duration.ofMinutes(5) : cooldownFallback;
if (minStartInterval.compareTo(Duration.ofSeconds(5)) < 0 || minStartInterval.compareTo(Duration.ofMinutes(5)) > 0
Duration minimumInterval = collectionMode == CollectionMode.VALIDATION ? Duration.ofSeconds(5) : Duration.ofMillis(1);
if (minStartInterval.compareTo(minimumInterval) < 0 || minStartInterval.compareTo(Duration.ofMinutes(5)) > 0
|| cooldownFallback.compareTo(Duration.ofSeconds(5)) < 0 || cooldownFallback.compareTo(Duration.ofDays(1)) > 0) {
throw new IllegalArgumentException("领星出口间隔至少5秒,冷却配置须为有界正值");
throw new IllegalArgumentException("领星 VALIDATION 出口间隔至少5秒,其他模式至少1毫秒;间隔和冷却配置须为有界正值");
}
if (budgetAuthorityBaseUri != null && (budgetAuthorityBaseUri.getHost() == null
|| !("http".equals(budgetAuthorityBaseUri.getScheme()) || "https".equals(budgetAuthorityBaseUri.getScheme()))
@@ -4,6 +4,8 @@ import com.baiyue.integration.config.LingxingEgressProperties;
import com.baiyue.integration.dto.lingxing.LingxingEgressDtos.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Clock;
import java.time.Instant;
import java.util.concurrent.Semaphore;
@@ -12,6 +14,7 @@ import java.util.concurrent.TimeUnit;
/** Local serialization supplements, never replaces, the externally attested window and durable budget. */
@Component
public final class LingxingEgressGate {
private static final Logger log = LoggerFactory.getLogger(LingxingEgressGate.class);
private final LingxingEgressProperties properties;
private final LingxingBudgetAuthority authority;
private final Clock clock;
@@ -73,15 +76,20 @@ public final class LingxingEgressGate {
try { result = call.run(handle); }
catch (LingxingWmsException error) { failure = error; }
catch (RuntimeException error) { failure = failure("LOCAL_PROCESSING_FAILED"); }
Instant callFinishedAt = clock.instant();
CompletionRequest completion = new CompletionRequest(context, operation, hash, handle.actualHttpAttempts,
failure == null ? null : failure.code(), failure == null ? Integer.valueOf(200) : failure.upstreamStatus(),
failure == null ? Integer.valueOf(200) : failure.sourceCode(), paused, cooldownUntil);
try { authority.complete(completion); }
boolean completionConfirmed = false;
try { authority.complete(completion); completionConfirmed = true; }
catch (RuntimeException error) {
recoverableRatePause = false; paused = true;
// Unconfirmed completion must remain distinguishable even when the provider already failed.
failure = failure("BUDGET_COMPLETION_UNCONFIRMED");
}
log.debug("lingxingAttempt operation={} start={} end={} upstreamStatus={} sourceCode={} errorCode={} completionConfirmed={}",
operation, handle.startedAt, callFinishedAt, completion.upstreamStatus(), completion.sourceCode(),
completion.errorCode(), completionConfirmed);
if (failure != null) throw failure.withEgress(handle.metadata());
return result;
} catch (InterruptedException ignored) {
@@ -112,12 +120,13 @@ public final class LingxingEgressGate {
private final EgressContext context;
private final Instant expiresAt;
private int actualHttpAttempts;
private Instant startedAt;
private AttemptHandle(EgressContext context, Instant expiresAt) { this.context = context; this.expiresAt = expiresAt; }
/** Immediately before send, after signing. One permit can never send twice. */
public void startHttp() {
check(context);
if (actualHttpAttempts != 0 || !clock.instant().isBefore(expiresAt)) throw failure("ATTEMPT_NOT_SENDABLE");
actualHttpAttempts = 1; lastStart = clock.instant();
actualHttpAttempts = 1; lastStart = clock.instant(); startedAt = lastStart;
}
public void rateLimited(Instant retryAfter) {
Instant fallback = clock.instant().plus(properties.cooldownFallback());