fix(self-operated): remove business collection deadline limits
This commit is contained in:
+11
-9
@@ -137,7 +137,7 @@ class CollectionQueryServiceTest {
|
||||
assertFalse(rows.isEmpty());for(var row:rows){assertTrue(row.executionEnabled());assertNull(row.disabledReason());assertNull(row.nextScheduledAt());
|
||||
assertEquals("INVENTORY".equals(row.dataFamily()),row.publicationEnabled());}
|
||||
config.setExclusiveGrantExpiresAtUtc(now.minusSeconds(1));
|
||||
for(var row:service.overview(q(null,null,null)).warehouses()){assertFalse(row.executionEnabled());assertEquals("EXCLUSIVE_GRANT_NOT_VALID",row.disabledReason());}
|
||||
for(var row:service.overview(q(null,null,null)).warehouses()){assertTrue(row.executionEnabled());assertNull(row.disabledReason());}
|
||||
config.setEnvironment("development");
|
||||
for(var row:service.overview(q(null,null,null)).warehouses()){assertFalse(row.executionEnabled());assertEquals("BUSINESS_ENVIRONMENT_REJECTED",row.disabledReason());}
|
||||
}
|
||||
@@ -157,9 +157,9 @@ class CollectionQueryServiceTest {
|
||||
assertEquals("EXCLUSIVE_GRANT_NOT_VALID",row.disabledReason());
|
||||
}
|
||||
config.setPriorEgressClosed(true);config.setContinuousBusinessCollectionEnabled(false);
|
||||
assertFalse(service.overview(q(null,null,null)).warehouses().get(0).executionEnabled());
|
||||
assertTrue(service.overview(q(null,null,null)).warehouses().get(0).executionEnabled());
|
||||
}
|
||||
@Test void continuousBudgetRequiresEnabledModeAndNoDeadlineWhileBoundedBudgetsStillExpire() {
|
||||
@Test void plannerIdentityStillNeedsSwitchButManualRecordedDeadlineDoesNotExpire() {
|
||||
enableBusiness("test");config.setContinuousBusinessCollectionEnabled(true);
|
||||
String continuous="{\"continuous\":true,\"startedAtUtc\":\""+now.minusDays(2)+"\",\"maxHttp\":0,\"http\":10,\"confirmedHttp\":10,\"deadlineAtUtc\":null,\"stopReason\":null}";
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous));
|
||||
@@ -167,7 +167,9 @@ class CollectionQueryServiceTest {
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous.replace("\"deadlineAtUtc\":null","\"deadlineAtUtc\":\""+now.plusDays(1)+"\"")));
|
||||
assertEquals("BUSINESS_SESSION_BUDGET_MISSING",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(businessBudget(now.minusMinutes(1).toString(),null,10)));
|
||||
assertEquals("BUSINESS_SESSION_DEADLINE",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
|
||||
assertNull(service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous.replace("\"continuous\":true","\"continuous\":false")));
|
||||
assertEquals(List.of("RETRY"),service.taskDetail("LXCOL-TASK",q(null,null,null)).task().allowedActions());
|
||||
config.setContinuousBusinessCollectionEnabled(false);
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous));
|
||||
assertEquals("CONTINUOUS_COLLECTION_DISABLED",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
|
||||
@@ -234,14 +236,14 @@ class CollectionQueryServiceTest {
|
||||
enableBusiness("production");when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(null));
|
||||
assertEquals(List.of("RETRY"),service.taskDetail("LXCOL-TASK",q(null,null,null)).task().allowedActions());
|
||||
}
|
||||
@Test void expiredBudgetOnlyAllowsProvenLocalWorkButStoppedBudgetNeverDoes() {
|
||||
@Test void recordedDeadlineDoesNotBlockRemoteOrLocalWorkButPersistedStopStillDoes() {
|
||||
enableBusiness("test");
|
||||
when(mapper.latestAttempts(eq("primary"),anyList())).thenReturn(List.of(run(1,"FAILED",null)));
|
||||
for(boolean ready:List.of(false,true)) {
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(businessBudget(now.toString(),null,5),ready));
|
||||
var task=service.taskDetail("LXCOL-TASK",q(null,null,null)).task();
|
||||
assertEquals(ready,task.latestAttempt().retryable());
|
||||
assertEquals(ready?null:"BUSINESS_SESSION_DEADLINE",task.actionBlockReason());
|
||||
assertTrue(task.latestAttempt().retryable());
|
||||
assertNull(task.actionBlockReason());
|
||||
}
|
||||
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(businessBudget(now.toString(),"BUSINESS_SESSION_DEADLINE",5),true));
|
||||
assertEquals("BUSINESS_SESSION_STOPPED",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
|
||||
@@ -253,8 +255,8 @@ class CollectionQueryServiceTest {
|
||||
now,null,now,null,"PARTIAL",null,null,code,null,now.minusMinutes(2),"A",1L,"hash");
|
||||
when(mapper.runs(any(),eq(upper))).thenReturn(List.of(attempt));
|
||||
String message=service.runs(q(null,null,null)).records().get(0).failureSummary();
|
||||
assertTrue(message.contains(code.endsWith("HTTP_LIMIT")?"请求预算已耗尽":"已到截止时间"));
|
||||
assertTrue(message.contains("不能恢复原任务"));
|
||||
assertTrue(message.contains(code.endsWith("HTTP_LIMIT")?"请求预算已耗尽":"旧采集截止时间"));
|
||||
assertTrue(message.contains(code.endsWith("HTTP_LIMIT")?"不能恢复原任务":"受控恢复"));
|
||||
}
|
||||
}
|
||||
Attempt run(int attempt,String status,String snapshot) {
|
||||
|
||||
+1
-1
@@ -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),120));
|
||||
stale.setBusinessBudgetJson(SelfopBusinessBudget.freeze(LocalDateTime.of(2026,10,13,0,0)));
|
||||
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
-7
@@ -17,23 +17,25 @@ class SelfopBusinessBudgetTest {
|
||||
}
|
||||
private final LocalDateTime start=LocalDateTime.of(2026,10,13,0,0);
|
||||
|
||||
@Test void serializedConsumptionAndDeadlineSurviveReload() {
|
||||
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,60),SelfopBusinessBudget.class);
|
||||
@Test void recordedFiniteDeadlineSurvivesReloadWithoutStoppingExistingBusiness() {
|
||||
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start),SelfopBusinessBudget.class);
|
||||
budget.deadlineAtUtc=start.plusSeconds(60);
|
||||
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(0,restored.maxHttp);assertNull(restored.limit(start.plusSeconds(1)));
|
||||
assertEquals("BUSINESS_SESSION_DEADLINE",restored.limit(start.plusSeconds(60)));
|
||||
assertNull(restored.limit(start.plusSeconds(60)));assertNull(restored.limit(start.plusYears(1)));
|
||||
assertFalse(restored.continuous);
|
||||
restored.stopReason="BUSINESS_SESSION_HTTP_LIMIT";
|
||||
assertEquals(restored.stopReason,restored.limit(start.plusDays(1)));
|
||||
}
|
||||
|
||||
@Test void unlimitedHttpRetainsStrictDeadlineAndAccounting() {
|
||||
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(null,60));
|
||||
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(start,0));
|
||||
@Test void newBusinessHasNoDeadlineAndRetainsStrictAccounting() {
|
||||
assertThrows(IllegalArgumentException.class,()->SelfopBusinessBudget.freeze(null));
|
||||
assertThrows(EgressRejectedException.class,()->new SelfopBusinessBudget().validate());
|
||||
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start,60),SelfopBusinessBudget.class);
|
||||
var budget=SelfopSupport.read(SelfopBusinessBudget.freeze(start),SelfopBusinessBudget.class);
|
||||
assertNull(budget.deadlineAtUtc);assertFalse(budget.continuous);
|
||||
budget.confirmedHttp=1;
|
||||
assertThrows(EgressRejectedException.class,budget::validate);
|
||||
budget.confirmedHttp=12001;budget.http=12001;budget.maxHttp=5000;
|
||||
|
||||
+48
-22
@@ -34,17 +34,18 @@ class SelfopEgressServiceTest {
|
||||
cfg.setCollectionMode("BUSINESS");cfg.setEnvironment("unknown");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_ENVIRONMENT_REJECTED");
|
||||
cfg.setCollectionMode("VALIDATION");cfg.setPriorEgressClosed(false);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");verifyNoInteractions(mapper);
|
||||
}
|
||||
@Test void testBusinessStillChargesPersistentAccountAndRequiresLiveExclusiveGrant(){
|
||||
@Test void businessStillChargesPersistentAccountAndRequiresMatchingGrantIdentity(){
|
||||
testBusiness(5,300);
|
||||
var c=reserve(Operation.WAREHOUSES,1,null,null);success(c,Operation.WAREHOUSES);
|
||||
assertThat(budget().reservedHttp).isEqualTo(1);assertThat(budget().confirmedHttp).isEqualTo(1);
|
||||
assertThat(businessBudget().http).isEqualTo(1);assertThat(businessBudget().confirmedHttp).isEqualTo(1);
|
||||
cfg.setExclusiveGrantExpiresAtUtc(now());
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
|
||||
assertThat(budget().reservedHttp).isEqualTo(1);
|
||||
success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
|
||||
cfg.setExclusiveGrantId("different");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
|
||||
assertThat(budget().reservedHttp).isEqualTo(2);
|
||||
}
|
||||
@Test void frozenPageSizeCannotBeExpanded(){assertThatThrownBy(()->reserve(Operation.WAREHOUSES,11,null,null)).hasMessage("VALIDATION_PAGE_SIZE");assertThat(budget().reservedHttp).isZero();}
|
||||
@Test void continuousBusinessGrantExpiryDoesNotClipShortPendingOrPermitAndSwitchKeepsCounters(){
|
||||
@Test void businessGrantExpiryDoesNotClipShortPendingOrPermitRegardlessOfPlannerSwitch(){
|
||||
testBusiness(5,300);success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
|
||||
cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(now().minusSeconds(1));cfg.setEgressLeaseSeconds(120);
|
||||
var c=reserve(Operation.WAREHOUSES,1,null,null);
|
||||
@@ -54,7 +55,8 @@ class SelfopEgressServiceTest {
|
||||
restarted.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));
|
||||
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(2);
|
||||
cfg.setContinuousBusinessCollectionEnabled(false);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
|
||||
clock.advance(5);success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
|
||||
assertThat(budget().reservedHttp).isEqualTo(3);assertThat(businessBudget().continuous).isFalse();
|
||||
}
|
||||
@Test void continuousFlagNeverBypassesValidationWindowGrantIdentityOrHandoff(){
|
||||
cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
|
||||
@@ -64,15 +66,15 @@ class SelfopEgressServiceTest {
|
||||
cfg.setExclusiveGrantId("grant");cfg.setPriorEgressClosed(false);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");verifyNoInteractions(mapper);
|
||||
}
|
||||
@Test void continuousGrantDoesNotRenewFiniteManualRootOrRelaxFence(){
|
||||
@Test void oldManualDeadlineDoesNotClipPermitOrRelaxFence(){
|
||||
testBusiness(5,10);cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
|
||||
account.setFenceToken(8L);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EGRESS_FENCE_LOST");
|
||||
account.setFenceToken(7L);var c=reserve(Operation.WAREHOUSES,1,null,null);
|
||||
assertThat(budget().pending.expiresAtUtc).isEqualTo(businessBudget().deadlineAtUtc);
|
||||
assertThat(budget().pending.expiresAtUtc).isAfter(businessBudget().deadlineAtUtc);
|
||||
clock.advance(10);
|
||||
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("BUSINESS_SESSION_DEADLINE");
|
||||
assertThat(budget().pending.consumed).isFalse();
|
||||
assertThat(service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER)).expiresAt()).isEqualTo(clock.instant().plusSeconds(30));
|
||||
assertThat(budget().pending.consumed).isTrue();assertThat(businessBudget().continuous).isFalse();
|
||||
}
|
||||
@Test void continuousBusinessAcrossMidnightRetainsUnknownPendingAndReviewAcrossRestart(){
|
||||
business();cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
|
||||
@@ -188,7 +190,9 @@ 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(),seconds));}
|
||||
private void testBusiness(int http,int seconds){business();cfg.setEnvironment("test");task.setBusinessRootTaskId(1L);
|
||||
var recorded=new SelfopBusinessBudget();recorded.startedAtUtc=now();recorded.deadlineAtUtc=now().plusSeconds(seconds);
|
||||
task.setBusinessBudgetJson(SelfopSupport.json(recorded));}
|
||||
private SelfopBusinessBudget businessBudget(){return SelfopSupport.read(task.getBusinessBudgetJson(),SelfopBusinessBudget.class);}
|
||||
private void continuousBusinessRoot(){
|
||||
business();cfg.setEnvironment("test");cfg.setContinuousBusinessCollectionEnabled(true);
|
||||
@@ -298,7 +302,7 @@ class SelfopEgressServiceTest {
|
||||
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);
|
||||
testBusiness(1,1);clock.advance(2);cfg.setExclusiveGrantExpiresAtUtc(now().minusSeconds(1));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");
|
||||
@@ -306,28 +310,50 @@ class SelfopEgressServiceTest {
|
||||
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");
|
||||
clock.advance(cfg.getCooldownSeconds()+1);var afterCooldown=reserve(Operation.WAREHOUSES,1,null,null);
|
||||
service.authorize(new AuthorizationRequest(afterCooldown,Operation.WAREHOUSES,PROVIDER));
|
||||
service.complete(new CompletionRequest(afterCooldown,Operation.WAREHOUSES,PROVIDER,1,"LINGXING_WMS_AUTHORIZATION_REJECTED",401,null,true,null));
|
||||
clock.advance(2);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("ACCOUNT_REQUIRES_REVIEW");
|
||||
assertThat(account.getApiPauseReason()).isEqualTo("AUTHORIZATION_REQUIRES_REVIEW");
|
||||
}
|
||||
@Test void testBusinessDeadlineClipsPermitAndAuthorizeRechecksBeforeHttp(){
|
||||
testBusiness(5,10);var c=reserve(Operation.WAREHOUSES,1,null,null);assertThat(budget().pending.expiresAtUtc).isEqualTo(businessBudget().deadlineAtUtc);
|
||||
@Test void oldManualDeadlineRemovalKeepsSingleFlightAndUnknownOutcomeProtection(){
|
||||
testBusiness(5,1);clock.advance(2);cfg.setExclusiveGrantExpiresAtUtc(null);
|
||||
var c=reserve(Operation.WAREHOUSES,1,null,null);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("ACCOUNT_REQUEST_INFLIGHT");
|
||||
service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));clock.advance(31);
|
||||
var restarted=new SelfopEgressService(mapper,cfg,clock);
|
||||
assertThatThrownBy(()->restarted.reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL)).hasMessage("ACCOUNT_COOLDOWN_WAIT");
|
||||
assertThat(account.getApiPauseReason()).isEqualTo("REQUEST_OUTCOME_UNKNOWN");assertThat(budget().pending.abandoned).isTrue();
|
||||
assertThat(businessBudget().http).isEqualTo(1);assertThat(businessBudget().confirmedHttp).isZero();
|
||||
}
|
||||
@Test void existingManualBusinessContinuesPastBothRecordedDeadlineAndGrantWithoutReset(){
|
||||
testBusiness(5,10);cfg.setExclusiveGrantExpiresAtUtc(now().plusSeconds(10));cfg.setContinuousBusinessCollectionEnabled(false);
|
||||
task.setRequestKey("manual:2026-10-06");task.setRequestedBy("operator");task.setRoutingManifestJson("{\"nextPage\":37,\"scan\":2}");
|
||||
var original=businessBudget();original.http=8820;original.confirmedHttp=8819;task.setBusinessBudgetJson(SelfopSupport.json(original));
|
||||
var c=reserve(Operation.WAREHOUSES,1,null,null);assertThat(budget().pending.expiresAtUtc).isAfter(original.deadlineAtUtc);
|
||||
clock.advance(9);var permit=service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
|
||||
assertThat(permit.expiresAt()).isEqualTo(businessBudget().deadlineAtUtc.toInstant(ZoneOffset.UTC));
|
||||
assertThat(permit.expiresAt()).isEqualTo(clock.instant().plusSeconds(30));
|
||||
clock.advance(2);service.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));
|
||||
assertThat(businessBudget().confirmedHttp).isEqualTo(1);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_DEADLINE");
|
||||
assertThat(businessBudget().stopReason).isEqualTo("BUSINESS_SESSION_DEADLINE");
|
||||
clock.advance(5);var restarted=new SelfopEgressService(mapper,cfg,clock);success(restarted.reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL),Operation.WAREHOUSES);
|
||||
assertThat(businessBudget().http).isEqualTo(8822);assertThat(businessBudget().confirmedHttp).isEqualTo(8821);
|
||||
assertThat(businessBudget().startedAtUtc).isEqualTo(original.startedAtUtc);assertThat(businessBudget().deadlineAtUtc).isEqualTo(original.deadlineAtUtc);
|
||||
assertThat(businessBudget().continuous).isFalse();assertThat(businessBudget().stopReason).isNull();
|
||||
assertThat(task.getId()).isEqualTo(1L);assertThat(task.getBusinessRootTaskId()).isEqualTo(1L);assertThat(task.getRequestKey()).isEqualTo("manual:2026-10-06");
|
||||
assertThat(task.getRequestedBy()).isEqualTo("operator");assertThat(task.getRoutingManifestJson()).isEqualTo("{\"nextPage\":37,\"scan\":2}");
|
||||
verify(mapper,never()).terminateBusinessTasks(any(),any());
|
||||
}
|
||||
@Test void testBusinessExpiredOrStoppedPreparedAttemptCannotAuthorize(){
|
||||
testBusiness(5,10);var c=reserve(Operation.WAREHOUSES,1,null,null);clock.advance(10);
|
||||
@Test void businessShortPendingExpiryStillRejectsAfterSessionDeadlineRemoved(){
|
||||
testBusiness(5,10);cfg.setEgressLeaseSeconds(20);var c=reserve(Operation.WAREHOUSES,1,null,null);clock.advance(20);
|
||||
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER)))
|
||||
.hasMessage("BUSINESS_SESSION_DEADLINE").satisfies(e->assertThat(((EgressRejectedException)e).sampleDone()).isFalse());
|
||||
.hasMessage("EGRESS_PERMIT_REPLAY_OR_EXPIRED").satisfies(e->assertThat(((EgressRejectedException)e).sampleDone()).isFalse());
|
||||
assertThat(budget().pending.consumed).isFalse();assertThat(businessBudget().http).isEqualTo(1);assertThat(businessBudget().confirmedHttp).isZero();
|
||||
assertThat(task.getStatus()).isEqualTo("DEAD");
|
||||
assertThat(task.getStatus()).isEqualTo("RUNNING");
|
||||
}
|
||||
@Test void testBusinessMissingBudgetOrWrongRootFailsClosedWithoutBackfill(){
|
||||
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(),300));task.setBusinessRootTaskId(999L);
|
||||
task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now()));task.setBusinessRootTaskId(999L);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
|
||||
assertThat(businessBudget().http).isZero();
|
||||
}
|
||||
|
||||
+26
-14
@@ -403,7 +403,7 @@ class SelfopTaskServiceTest {
|
||||
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).isZero();
|
||||
assertThat(budget.deadlineAtUtc).isEqualTo(budget.startedAtUtc.plusSeconds(91));
|
||||
assertThat(budget.deadlineAtUtc).isNull();assertThat(budget.continuous).isFalse();
|
||||
var child=task("READY");child.setId(42L);child.setBusinessRootTaskId(root.getId());
|
||||
assertThat(SelfopRequestContext.forTask(child,run("FETCHING"),config).rootTaskId()).isEqualTo(root.getId());
|
||||
config.setEnvironment("production");
|
||||
@@ -431,19 +431,29 @@ class SelfopTaskServiceTest {
|
||||
assertThat(read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class).http).isEqualTo(7);
|
||||
verify(runs).insert(next);
|
||||
}
|
||||
@Test void expiredFetchCannotBeginAndManualRetryCannotRestartItsSession() {
|
||||
@Test void existingManualDiscoveryPastDeadlineBeginsFromPersistedCheckpointAndIdentity() {
|
||||
var root=boundedRoot(2,utcNow().minusSeconds(1));
|
||||
assertThatThrownBy(()->service.begin(1L,lease)).isInstanceOf(SelfopTaskService.BusinessSessionStoppedException.class);
|
||||
assertThatThrownBy(()->service.retry(1L,"new-key","operator","retry"))
|
||||
.isInstanceOf(SelfopTaskService.BusinessSessionStoppedException.class);
|
||||
verify(runs,never()).insert(any(SelfopSyncRun.class));verify(tasks,never()).insert(any(SelfopSyncTask.class));
|
||||
assertThat(read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class).http).isEqualTo(2);
|
||||
root.setTaskRole("ACCOUNT_DISCOVERY");root.setScopeId(null);root.setWarehouseCode(null);root.setDiscoveryScopeId(99L);
|
||||
root.setRequestKey("manual:2026-10-06");root.setTaskType("BACKFILL");root.setAttemptCount(3);
|
||||
root.setQueryFromUtc(LocalDateTime.of(2026,10,5,16,0));root.setQueryUntilUtc(LocalDateTime.of(2026,10,6,16,0));
|
||||
var manifest=new SelfopRoutingManifest();manifest.setNextPage(37);manifest.setScan(2);manifest.setPages(100);manifest.setTotal(1000L);
|
||||
root.setRoutingManifestJson(json(manifest));String checkpoint=root.getRoutingManifestJson(),budget=root.getBusinessBudgetJson();
|
||||
var account=new SelfopOutboundDiscoveryScope();account.setId(99L);when(locks.lockDiscovery(99L)).thenReturn(account);
|
||||
var next=service.begin(1L,new SelfopLeaseService.Lease(99L,"owner",7L,true));
|
||||
assertThat(next.getAttemptNo()).isEqualTo(4);assertThat(next.getTaskId()).isEqualTo(1L);assertThat(next.getStatus()).isEqualTo("FETCHING");
|
||||
assertThat(root.getRoutingManifestJson()).isEqualTo(checkpoint);assertThat(root.getBusinessBudgetJson()).isEqualTo(budget);
|
||||
assertThat(root.getBusinessRootTaskId()).isEqualTo(1L);assertThat(root.getRequestKey()).isEqualTo("manual:2026-10-06");
|
||||
assertThat(root.getRequestedBy()).isEqualTo("operator");assertThat(root.getQueryFromUtc()).isEqualTo(LocalDateTime.of(2026,10,5,16,0));
|
||||
verify(runs).insert(next);verify(tasks,never()).insert(any(SelfopSyncTask.class));
|
||||
}
|
||||
@Test void expiredYieldAndBudgetWaitEndInsteadOfSchedulingAnotherDispatch() {
|
||||
boundedRoot(2,utcNow().minusSeconds(1));
|
||||
assertThatThrownBy(()->service.yield(1L,20L,lease)).isInstanceOf(SelfopTaskService.BusinessSessionStoppedException.class);
|
||||
assertThatThrownBy(()->service.waitForBudget(1L,20L,utcNow().plusDays(1),lease)).isInstanceOf(SelfopTaskService.BusinessSessionStoppedException.class);
|
||||
verify(tasks,never()).updateById(any(SelfopSyncTask.class));verify(runs,never()).updateById(any(SelfopSyncRun.class));
|
||||
@Test void pastRecordedDeadlineYieldAndBudgetWaitKeepSchedulingSameRoot() {
|
||||
var root=boundedRoot(2,utcNow().minusSeconds(1));root.setStatus("RUNNING");
|
||||
var active=run("FETCHING");when(locks.lockRun(20L)).thenReturn(active);String budget=root.getBusinessBudgetJson();
|
||||
service.yield(1L,20L,lease);assertThat(root.getStatus()).isEqualTo("RETRY_WAIT");assertThat(active.getStatus()).isEqualTo("YIELDED");
|
||||
root.setStatus("RUNNING");active.setStatus("FETCHING");var until=utcNow().plusMinutes(5);
|
||||
service.waitForBudget(1L,20L,until,lease);
|
||||
assertThat(root.getNextRetryAtUtc()).isEqualTo(until);assertThat(root.getBusinessBudgetJson()).isEqualTo(budget);
|
||||
verify(tasks,never()).update(any(),any());
|
||||
}
|
||||
@Test void continuousFailureStopsInsteadOfBecomingAnotherScheduledRetry(){
|
||||
var root=boundedRoot(0,utcNow().plusMinutes(3));config.setContinuousBusinessCollectionEnabled(true);
|
||||
@@ -467,7 +477,7 @@ class SelfopTaskServiceTest {
|
||||
assertThat(read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class).stopReason).isEqualTo("BUSINESS_SESSION_HTTP_LIMIT");
|
||||
}
|
||||
@Test void manualRetryAuditRetainsOriginalRootAndCannotFreezeAnotherAllowance() {
|
||||
var root=boundedRoot(3,utcNow().plusMinutes(3));root.setStatus("RETRY_WAIT");String frozen=root.getBusinessBudgetJson();
|
||||
var root=boundedRoot(3,utcNow().minusSeconds(1));root.setStatus("RETRY_WAIT");String frozen=root.getBusinessBudgetJson();
|
||||
config.setRequestBudget(100);config.setTimeBudgetSeconds(900);
|
||||
assertThat(service.retry(1L,"retry-bounded","operator","retry failed attempt")).isSameAs(root);
|
||||
var saved=org.mockito.ArgumentCaptor.forClass(SelfopSyncTask.class);verify(tasks).insert(saved.capture());
|
||||
@@ -489,8 +499,10 @@ class SelfopTaskServiceTest {
|
||||
order.verify(locks,times(2)).lockTask(1L);
|
||||
}
|
||||
|
||||
@Test void incompleteManifestCannotUseLocalWorkExceptionAfterDeadline() {
|
||||
@Test void incompleteManifestNeverBypassesExplicitFailureStop() {
|
||||
var root=boundedRoot(1,utcNow().minusSeconds(1));when(scopes.selectById(10L)).thenReturn(scope("OUTBOUND"));
|
||||
var budget=read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class);
|
||||
budget.stopReason="AUTHORIZATION_REJECTED";root.setBusinessBudgetJson(json(budget));
|
||||
for(String manifest:List.of("{\"directoryComplete\":true}","{\"directoryComplete\":true,\"objects\":[]}",
|
||||
"{\"directoryComplete\":true,\"objects\":{\"one\":{\"effectiveStageId\":\" \"}}}")) {
|
||||
root.setRoutingManifestJson(manifest);
|
||||
|
||||
Reference in New Issue
Block a user