fix(selfop): remove business request count quotas
This commit is contained in:
+2
@@ -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;
|
||||
|
||||
+3
-2
@@ -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";
|
||||
|
||||
+7
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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");
|
||||
|
||||
+1
-1
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user