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
@@ -97,6 +97,29 @@ class CollectionQueryServiceTest {
result=service.taskDetail(t.taskNo(),q(null,null,null)).task();
assertEquals("UNAVAILABLE",result.stageEstimate().status());assertNull(result.stageEstimate().remainingSeconds());
}
@Test void businessOverviewAndRetryIgnoreCumulativeRequestCapsButKeepCounters() {
enableBusiness("test");
when(mapper.protection("primary")).thenReturn(new Protection(null,null,
"{\"budgetDate\":\"2026-10-12\",\"dailyLimit\":100,\"reservedHttp\":12001,\"confirmedHttp\":12000}"));
for(int configured:List.of(100,0)) {
config.setDailyRequestBudget(configured);
for(var row:service.overview(q(null,null,null)).warehouses()) {
assertTrue(row.executionEnabled());assertNull(row.disabledReason());
assertEquals(0,row.protection().dailyRequestBudget());
assertEquals(12001L,row.protection().reservedHttpRequests());
assertEquals(12000L,row.protection().confirmedHttpRequests());
}
}
for(int maximum:List.of(5,0)) {
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(
businessBudget(now.plusHours(1).toString(),null,6000).replace("\"maxHttp\":5","\"maxHttp\":"+maximum)));
assertEquals(List.of("RETRY"),service.taskDetail("LXCOL-TASK",q(null,null,null)).task().allowedActions());
}
config.setCollectionMode("VALIDATION");config.setDailyRequestBudget(100);
for(var row:service.overview(q(null,null,null)).warehouses()) {
assertFalse(row.executionEnabled());assertEquals("DAILY_BUDGET_EXHAUSTED",row.disabledReason());
}
}
@Test void controlledTestBusinessProjectsEnabledInventoryPublicationWithoutAutomaticSchedule() {
config.setCollectionMode("BUSINESS");config.setEnvironment("test");config.setQuantityUnit("PCS");
config.setExclusiveHandoffVerified(true);config.setPriorEgressClosed(true);config.setInflightDrained(true);config.setTransferredBudgetVerified(true);
@@ -150,7 +173,7 @@ class CollectionQueryServiceTest {
valid.replace(now.minusHours(1).toString(),now.plusHours(1).toString()),
valid.replace(now.minusHours(1).toString(),now.plusHours(2).toString()),
valid.replace("\"maxHttp\":5","\"maxHttp\":2147483648"),
valid.replace("\"http\":5","\"http\":6"),
valid.replace("\"http\":5","\"http\":2147483648"),
valid.replace("\"confirmedHttp\":5","\"confirmedHttp\":6"))) {
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(budget,true));
var task=service.taskDetail("LXCOL-TASK",q(null,null,null)).task();
@@ -28,7 +28,7 @@ class SelfopBusinessBudgetPersistenceTest {
}
var tasks=session.getMapper(SelfopSyncTaskMapper.class);
var stale=new SelfopSyncTask();stale.setId(1L);stale.setStatus("RUNNING");stale.setBusinessRootTaskId(1L);
stale.setBusinessBudgetJson(SelfopBusinessBudget.freeze(LocalDateTime.of(2026,10,13,0,0),2,120));
stale.setBusinessBudgetJson(SelfopBusinessBudget.freeze(LocalDateTime.of(2026,10,13,0,0),120));
assertEquals(1,tasks.insert(stale)); // NEVER updates must still allow initial freezing.
var charged=SelfopSupport.read(stale.getBusinessBudgetJson(),SelfopBusinessBudget.class);
charged.http=2;charged.confirmedHttp=1;
@@ -9,25 +9,26 @@ class SelfopBusinessBudgetTest {
private final LocalDateTime start=LocalDateTime.of(2026,10,13,0,0);
@Test void serializedConsumptionAndDeadlineSurviveReload() {
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,2,60),SelfopBusinessBudget.class);
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,60),SelfopBusinessBudget.class);
budget.http=2;budget.confirmedHttp=1;
var restored=SelfopSupport.read(SelfopSupport.json(budget),SelfopBusinessBudget.class);
assertEquals(start.plusSeconds(60),restored.deadlineAtUtc);
assertEquals(2,restored.http);assertEquals(1,restored.confirmedHttp);
assertEquals("BUSINESS_SESSION_HTTP_LIMIT",restored.limit(start.plusSeconds(1)));
assertEquals(0,restored.maxHttp);assertNull(restored.limit(start.plusSeconds(1)));
assertEquals("BUSINESS_SESSION_DEADLINE",restored.limit(start.plusSeconds(60)));
restored.stopReason="BUSINESS_SESSION_HTTP_LIMIT";
assertEquals(restored.stopReason,restored.limit(start.plusDays(1)));
}
@Test void noImplicitUnlimitedOrInvalidAccounting() {
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(start,0,60));
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(start,1,0));
@Test void unlimitedHttpRetainsStrictDeadlineAndAccounting() {
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(null,60));
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(start,0));
assertThrows(EgressRejectedException.class,()->new SelfopBusinessBudget().validate());
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,2,60),SelfopBusinessBudget.class);
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,60),SelfopBusinessBudget.class);
budget.confirmedHttp=1;
assertThrows(EgressRejectedException.class,budget::validate);
budget.confirmedHttp=0;budget.http=3;
assertThrows(EgressRejectedException.class,budget::validate);
budget.confirmedHttp=12001;budget.http=12001;budget.maxHttp=5000;
assertDoesNotThrow(budget::validate);assertNull(budget.limit(start.plusSeconds(1)));
budget.http=-1;assertThrows(EgressRejectedException.class,budget::validate);
}
}
@@ -148,24 +148,31 @@ class SelfopEgressServiceTest {
}
}
private void business(){cfg.setCollectionMode("BUSINESS");cfg.setEnvironment("production");task.setExecutionPurpose("BUSINESS");run.setExecutionPurpose("BUSINESS");}
private void testBusiness(int http,int seconds){business();cfg.setEnvironment("test");task.setBusinessRootTaskId(1L);task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now(),http,seconds));}
private void testBusiness(int http,int seconds){business();cfg.setEnvironment("test");task.setBusinessRootTaskId(1L);task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now(),seconds));}
private SelfopBusinessBudget businessBudget(){return SelfopSupport.read(task.getBusinessBudgetJson(),SelfopBusinessBudget.class);}
@Test void testBusinessLastReservedAttemptIsAllowedAndRestartCannotRenewSession(){
testBusiness(1,300);var start=businessBudget().startedAtUtc;var deadline=businessBudget().deadlineAtUtc;
var c=reserve(Operation.WAREHOUSES,1,null,null);assertThat(businessBudget().http).isEqualTo(1);
service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
@Test void businessSurpassesPriorDailyAndRootHttpCapsAcrossRestartWithoutRenewingDeadline(){
testBusiness(5000,300);cfg.setDailyRequestBudget(12000);cfg.setTransferredDailyHttpCount(12000);
var rootBudget=businessBudget();rootBudget.maxHttp=5000;rootBudget.http=5000;rootBudget.confirmedHttp=5000;task.setBusinessBudgetJson(SelfopSupport.json(rootBudget));
var start=rootBudget.startedAtUtc;var deadline=rootBudget.deadlineAtUtc;
var c=reserve(Operation.WAREHOUSES,1,null,null);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
var completion=new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null);
service.complete(completion);new SelfopEgressService(mapper,cfg,clock).complete(completion);
assertThat(businessBudget().confirmedHttp).isEqualTo(1);clock.advance(10);
cfg.setRequestBudget(1000);cfg.setTimeBudgetSeconds(3600);cfg.setExclusiveGrantId("renewed");
var renewed=new SelfopRequestContext("synthetic","BUSINESS",1L,"selfop-1","renewed",1L,2L);
assertThatThrownBy(()->new SelfopEgressService(mapper,cfg,clock).reserve(renewed,Operation.OUTBOUND_QUERY,1,null,null,INTERNAL))
.hasMessage("BUSINESS_SESSION_HTTP_LIMIT").satisfies(e->{assertThat(((EgressRejectedException)e).sampleDone()).isFalse();assertThat(((EgressRejectedException)e).waitUntilUtc()).isNull();});
assertThat(task.getStatus()).isEqualTo("DEAD");assertThat(task.getNextRetryAtUtc()).isNull();
assertThat(businessBudget().startedAtUtc).isEqualTo(start);assertThat(businessBudget().deadlineAtUtc).isEqualTo(deadline);
assertThat(businessBudget().maxHttp).isEqualTo(1);assertThat(budget().reservedHttp).isEqualTo(1);
verify(mapper).terminateBusinessTasks(1L,"synthetic");verify(mapper).terminateBusinessRuns(eq(1L),eq("synthetic"),eq("BUSINESS_SESSION_HTTP_LIMIT"),any());
verify(mapper,never()).terminateValidationRuns(any(),any(),any(),any());
assertThat(businessBudget().http).isEqualTo(5001);assertThat(businessBudget().confirmedHttp).isEqualTo(5001);
assertThat(budget().dailyLimit).isZero();assertThat(budget().reservedHttp).isEqualTo(12001);assertThat(budget().confirmedHttp).isEqualTo(1);
clock.advance(10);cfg.setRequestBudget(1000);cfg.setTimeBudgetSeconds(3600);
var next=new SelfopEgressService(mapper,cfg,clock).reserve(context(),Operation.OUTBOUND_QUERY,1,null,null,INTERNAL);success(next,Operation.OUTBOUND_QUERY);
assertThat(businessBudget().http).isEqualTo(5002);assertThat(businessBudget().startedAtUtc).isEqualTo(start);assertThat(businessBudget().deadlineAtUtc).isEqualTo(deadline);
verify(mapper,never()).terminateBusinessTasks(any(),any());verify(mapper,never()).terminateValidationRuns(any(),any(),any(),any());
}
@Test void businessWithoutDailyQuotaStillUsesIntervalAndActual429Review(){
testBusiness(1,300);cfg.setDailyRequestBudget(0);cfg.setBusinessMinRequestIntervalMs(1200);
var c=reserve(Operation.WAREHOUSES,1,null,null);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
service.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));
assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_INTERVAL_WAIT");
clock.advance(2);var next=reserve(Operation.OUTBOUND_QUERY,1,null,null);service.authorize(new AuthorizationRequest(next,Operation.OUTBOUND_QUERY,PROVIDER));
service.complete(new CompletionRequest(next,Operation.OUTBOUND_QUERY,PROVIDER,1,"LINGXING_WMS_HTTP_RATE_LIMITED",429,null,true,null));
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(2);assertThat(budget().rateLimitFailures).isEqualTo(1);
assertThat(account.getApiPauseReason()).isEqualTo("RATE_LIMIT_COOLDOWN");
}
@Test void testBusinessDeadlineClipsPermitAndAuthorizeRechecksBeforeHttp(){
testBusiness(5,10);var c=reserve(Operation.WAREHOUSES,1,null,null);assertThat(budget().pending.expiresAtUtc).isEqualTo(businessBudget().deadlineAtUtc);
@@ -187,7 +194,7 @@ class SelfopEgressServiceTest {
testBusiness(5,300);task.setBusinessBudgetJson(null);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
assertThat(task.getBusinessBudgetJson()).isNull();assertThat(budget().reservedHttp).isZero();
task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now(),5,300));task.setBusinessRootTaskId(999L);
task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now(),300));task.setBusinessRootTaskId(999L);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
assertThat(businessBudget().http).isZero();
}
@@ -201,8 +208,8 @@ class SelfopEgressServiceTest {
var c=new SelfopEgressService(mapper,cfg,clock).reserve(context,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL);success(c,Operation.PRODUCT_INVENTORY);
assertThat(businessBudget().http).isEqualTo(2);assertThat(businessBudget().confirmedHttp).isEqualTo(2);assertThat(child.getBusinessBudgetJson()).isNull();
child.setBusinessRootTaskId(999L);assertThatThrownBy(()->service.reserve(context,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
child.setBusinessRootTaskId(1L);assertThatThrownBy(()->service.reserve(context,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL)).hasMessage("BUSINESS_SESSION_HTTP_LIMIT");
verify(mapper).terminateBusinessTasks(1L,"synthetic");
child.setBusinessRootTaskId(1L);success(service.reserve(context,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL),Operation.PRODUCT_INVENTORY);
assertThat(businessBudget().http).isEqualTo(3);verify(mapper,never()).terminateBusinessTasks(any(),any());
}
@Test void productionBusinessDoesNotRequireTestBudget(){
business();success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
@@ -371,7 +371,7 @@ class SelfopTaskServiceTest {
var root=task("READY");root.setRequestKey("manual:frozen");root.setRequestedBy("operator");
service.create(root);
var budget=read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class);
assertThat(root.getBusinessRootTaskId()).isEqualTo(root.getId());assertThat(budget.maxHttp).isEqualTo(7);
assertThat(root.getBusinessRootTaskId()).isEqualTo(root.getId());assertThat(budget.maxHttp).isZero();
assertThat(budget.deadlineAtUtc).isEqualTo(budget.startedAtUtc.plusSeconds(91));
var child=task("READY");child.setId(42L);child.setBusinessRootTaskId(root.getId());
assertThat(SelfopRequestContext.forTask(child,run("FETCHING"),config).rootTaskId()).isEqualTo(root.getId());