fix(selfop): remove business request count quotas

This commit is contained in:
2026-10-13 20:02:07 +08:00
parent f151e74f5c
commit c2bb769676
11 changed files with 80 additions and 46 deletions
@@ -24,6 +24,7 @@ public class SelfopAnalyticsProperties {
private LocalDate transferredBudgetDate;
private LocalDateTime transferredCooldownUntilUtc;
private String transferredPauseReason;
/** VALIDATION-only daily ceiling; BUSINESS retains counts without a daily quota. */
private int dailyRequestBudget = 0;
private int businessMinRequestIntervalMs = 5000;
private int maxRateLimitFailures = 5;
@@ -43,6 +44,7 @@ public class SelfopAnalyticsProperties {
private String sourceAccount = "";
private String sourceQueryTimezone;
private String quantityUnit;
/** Per-dispatch slice size, never a BUSINESS root/session HTTP-count ceiling. */
private int requestBudget = 100;
private int timeBudgetSeconds = 120;
private int minRequestIntervalMs = 1100;
@@ -295,7 +295,7 @@ public class CollectionQueryService {
JsonNode budget=boundedObject(task.businessRootBudgetJson());
Long maximum=number(budget,"maxHttp"),reserved=number(budget,"http"),confirmed=number(budget,"confirmedHttp");
Instant started=budgetInstant(budget,"startedAtUtc"),deadline=budgetInstant(budget,"deadlineAtUtc");
if(budget==null||maximum==null||maximum<=0||maximum>Integer.MAX_VALUE||reserved==null||reserved>maximum
if(budget==null||maximum==null||maximum<0||maximum>Integer.MAX_VALUE||reserved==null||reserved>Integer.MAX_VALUE
||confirmed==null||confirmed>reserved||started==null||deadline==null||!deadline.isAfter(started))
return "BUSINESS_SESSION_BUDGET_MISSING";
JsonNode stopped=budget.get("stopReason");
@@ -332,7 +332,7 @@ public class CollectionQueryService {
var p=mapper.protection(account);
JsonNode budget=p==null?null:boundedObject(p.apiBudgetJson());
return new CollectionResponses.Protection(p==null?null:utc(p.apiCooldownUntilUtc()),p==null?null:p.apiPauseReason(),
config.getDailyRequestBudget(),number(budget,"reservedHttp"),number(budget,"confirmedHttp"),
"BUSINESS".equals(config.getCollectionMode())?0:config.getDailyRequestBudget(),number(budget,"reservedHttp"),number(budget,"confirmedHttp"),
budget==null||!budget.path("budgetDate").isTextual()?null:budget.path("budgetDate").textValue());
}
private String executionBlock(String warehouse,CollectionResponses.Protection protection) {
@@ -347,6 +347,7 @@ public class CollectionQueryService {
if(protection.accountCooldownUntil()!=null&&clock.instant().isBefore(protection.accountCooldownUntil()))return "ACCOUNT_COOLDOWN";
if(!config.isExclusiveHandoffVerified()||!config.isPriorEgressClosed()||!config.isInflightDrained()||!config.isTransferredBudgetVerified()
||config.getExclusiveGrantExpiresAtUtc()==null||!clock.instant().isBefore(utc(config.getExclusiveGrantExpiresAtUtc())))return "EXCLUSIVE_GRANT_NOT_VALID";
if("BUSINESS".equals(config.getCollectionMode()))return null;
if(config.getDailyRequestBudget()<=0)return "DAILY_BUDGET_NOT_CONFIGURED";
if(protection.reservedHttpRequests()!=null&&clock.instant().atZone(BUSINESS_ZONE).toLocalDate().toString().equals(protection.budgetDate())
&&protection.reservedHttpRequests()>=config.getDailyRequestBudget())return "DAILY_BUDGET_EXHAUSTED";
@@ -3,24 +3,25 @@ package com.baiyue.lastleg.selfoperated.analytics.request;
import com.baiyue.lastleg.selfoperated.analytics.model.SelfopSupport;
import java.time.LocalDateTime;
/** TEST manual BUSINESS session ceiling, frozen on its root and shared by descendants. */
/** BUSINESS session accounting and time ceiling, shared by the root and descendants. */
public final class SelfopBusinessBudget {
public int maxHttp, http, confirmedHttp;
/** Recorded field only; BUSINESS has no HTTP-count ceiling. New sessions write zero. */
public int maxHttp;
public int http, confirmedHttp;
public LocalDateTime startedAtUtc, deadlineAtUtc;
public String stopReason;
public static String freeze(LocalDateTime now, int maxHttp, int seconds) {
if (now == null || maxHttp < 1 || seconds < 1)
public static String freeze(LocalDateTime now, int seconds) {
if (now == null || seconds < 1)
throw new IllegalArgumentException("BUSINESS_SESSION_BUDGET_INVALID");
var budget = new SelfopBusinessBudget();
budget.maxHttp = maxHttp;
budget.startedAtUtc = now;
budget.deadlineAtUtc = now.plusSeconds(seconds);
return SelfopSupport.json(budget);
}
public void validate() {
if (maxHttp < 1 || http < 0 || http > maxHttp || confirmedHttp < 0 || confirmedHttp > http
if (http < 0 || confirmedHttp < 0 || confirmedHttp > http
|| startedAtUtc == null || deadlineAtUtc == null || !deadlineAtUtc.isAfter(startedAtUtc)
|| stopReason != null && !stopReason.matches("[A-Z0-9_]{1,64}"))
throw new EgressRejectedException("BUSINESS_SESSION_BUDGET_INVALID", false, null);
@@ -30,7 +31,6 @@ public final class SelfopBusinessBudget {
validate();
if (stopReason != null) return stopReason;
if (!now.isBefore(deadlineAtUtc)) return "BUSINESS_SESSION_DEADLINE";
if (http >= maxHttp) return "BUSINESS_SESSION_HTTP_LIMIT";
return null;
}
}
@@ -39,7 +39,7 @@ public class SelfopEgressService {
}
int interval="VALIDATION".equals(context.executionPurpose())?Math.max(5000,budget.minIntervalMs):budget.minIntervalMs;
if(budget.lastRequestAtUtc!=null&&now.isBefore(budget.lastRequestAtUtc.plusNanos((long)interval*1000000)))throw new EgressRejectedException("ACCOUNT_INTERVAL_WAIT",false,budget.lastRequestAtUtc.plusNanos((long)interval*1000000));
if(budget.reservedHttp>=budget.dailyLimit)throw new EgressRejectedException("ACCOUNT_DAILY_BUDGET",false,now.plusHours(8).toLocalDate().plusDays(1).atStartOfDay().minusHours(8));
if("VALIDATION".equals(context.executionPurpose())&&budget.reservedHttp>=budget.dailyLimit)throw new EgressRejectedException("ACCOUNT_DAILY_BUDGET",false,now.plusHours(8).toLocalDate().plusDays(1).atStartOfDay().minusHours(8));
var outbound=new EgressContext(context.sourceAccount(),context.executionPurpose(),String.valueOf(context.rootTaskId()),context.sessionId(),UUID.randomUUID().toString(),context.exclusiveGrantId());
var pending=new SelfopApiBudget.Pending();pending.context=outbound;pending.operation=operation;pending.internalRequestHash=requestHash;pending.taskId=context.taskId();pending.runId=context.runId();pending.expiresAtUtc=now.plusSeconds(Math.max(1,Math.min(120,config.getEgressLeaseSeconds())));
if(pending.expiresAtUtc.isAfter(config.getExclusiveGrantExpiresAtUtc()))pending.expiresAtUtc=config.getExclusiveGrantExpiresAtUtc();
@@ -58,8 +58,7 @@ public class SelfopEgressService {
var task=mapper.lockTask(pending.taskId);var run=mapper.lockRun(pending.runId);
if(testBusiness(c.executionPurpose())){
var b=business(root);verifyBusinessTask(task,root);
// The last reservation already spent its HTTP allowance. Recheck time/stop,
// not http>=maxHttp, or that paid reservation could never be authorized.
// Authorization preserves the root time/stop fence independently of request accounting.
String limit=b.stopReason!=null?b.stopReason:!now.isBefore(b.deadlineAtUtc)?"BUSINESS_SESSION_DEADLINE":null;
if(limit!=null){stopBusiness(root,b,limit,now);throw reject(limit);}
if(pending.expiresAtUtc.isAfter(b.deadlineAtUtc))pending.expiresAtUtc=b.deadlineAtUtc;
@@ -128,7 +127,7 @@ public class SelfopEgressService {
if("BUSINESS".equals(purpose)&&!("production".equals(config.getEnvironment())||"test".equals(config.getEnvironment())))throw reject("BUSINESS_ENVIRONMENT_REJECTED");
if(!Objects.equals(config.getSourceAccount(),account)||account==null||account.isBlank())throw reject("SOURCE_ACCOUNT_MISMATCH");
if(grant==null||!Objects.equals(grant,config.getExclusiveGrantId())||config.getExclusiveGrantExpiresAtUtc()==null||!now.isBefore(config.getExclusiveGrantExpiresAtUtc())||!config.isExclusiveHandoffVerified()||!config.isPriorEgressClosed()||!config.isInflightDrained()||!config.isTransferredBudgetVerified())throw reject("EXCLUSIVE_HANDOFF_UNCONFIRMED");
if(config.getDailyRequestBudget()<1||config.getBusinessMinRequestIntervalMs()<1||config.getRequestBudget()<1||config.getTimeBudgetSeconds()<1||config.getMaxRateLimitFailures()<1||config.getCooldownSeconds()<1)throw reject("ACCOUNT_BUDGET_UNCONFIGURED");
if(("VALIDATION".equals(purpose)&&config.getDailyRequestBudget()<1)||config.getBusinessMinRequestIntervalMs()<1||config.getRequestBudget()<1||config.getTimeBudgetSeconds()<1||config.getMaxRateLimitFailures()<1||config.getCooldownSeconds()<1)throw reject("ACCOUNT_BUDGET_UNCONFIGURED");
}
private SelfopOutboundDiscoveryScope account(String id){var a=mapper.lockAccount(id);if(a==null)throw reject("EGRESS_ACCOUNT_MISSING");return a;}
private SelfopApiBudget budget(SelfopOutboundDiscoveryScope account,LocalDateTime now){
@@ -136,8 +135,9 @@ public class SelfopEgressService {
if(b.reservedHttp<0||b.confirmedHttp<0||b.confirmedHttp>b.reservedHttp)throw reject("ACCOUNT_BUDGET_INVALID");
LocalDate day=now.plusHours(8).toLocalDate();
if(!Objects.equals(day,b.budgetDate)){/* unknown in-flight is charged again on new day; cooling survives */b.budgetDate=day;b.dailyLimit=config.getDailyRequestBudget();b.reservedHttp=b.pending!=null&&!b.pending.completed?1:0;b.confirmedHttp=0;}
if(b.dailyLimit<1)b.dailyLimit=config.getDailyRequestBudget();
b.dailyLimit=Math.min(b.dailyLimit,config.getDailyRequestBudget());b.minIntervalMs=Math.max(b.minIntervalMs,config.getBusinessMinRequestIntervalMs());
if("BUSINESS".equals(config.getCollectionMode()))b.dailyLimit=0;
else {if(b.dailyLimit<1)b.dailyLimit=config.getDailyRequestBudget();b.dailyLimit=Math.min(b.dailyLimit,config.getDailyRequestBudget());}
b.minIntervalMs=Math.max(b.minIntervalMs,config.getBusinessMinRequestIntervalMs());
b.configHash=SelfopSupport.hash(Arrays.asList(b.dailyLimit,b.minIntervalMs,config.getRequestBudget(),config.getTimeBudgetSeconds()));
if(!Objects.equals(config.getExclusiveGrantId(),b.importedGrantId)){
if(config.getTransferredBudgetDate()==null||!config.getTransferredBudgetDate().equals(day)||config.getTransferredDailyHttpCount()<0)throw reject("HANDOFF_BUDGET_DATE_UNCONFIRMED");
@@ -113,7 +113,7 @@ public class SelfopTaskService {
if(task.getRequestKey().startsWith("manual:")&&task.getUpstreamTaskId()==null) {
require(controlledBusinessTask(task,tasks::selectById),"SELFOP_TEST_MANUAL_TASK_REQUIRED");
task.setBusinessRootTaskId(task.getId());
task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now,config.getRequestBudget(),config.getTimeBudgetSeconds()));
task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now,config.getTimeBudgetSeconds()));
}else {
var parent=businessParent(task);
require(parent!=null&&Objects.equals(parent.getSourceAccount(),task.getSourceAccount())&&controlledBusinessTask(parent,tasks::selectById),"SELFOP_TEST_MANUAL_TASK_REQUIRED");