Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
128 commits
Select commit Hold shift + click to select a range
4da84e9
feat(managed-agent): admit later Turns of a Workspace-bound Session f…
Sep 30, 2026
1dc0d0c
feat(web-shell): let the creator send later Turns to a bound Session
Sep 30, 2026
aedd7a6
feat(managed-agent): let the bound Session creator cancel a running Turn
Sep 30, 2026
e12dc74
feat(managed-agent): let the bound Session creator rename it
Sep 30, 2026
3df9ee1
style(web-shell): keep the workspaceTurns expression on one line as P…
Sep 30, 2026
a550540
test(managed-agent): read the renamed title from the public Session m…
Sep 30, 2026
b8c5830
fix(web-shell): match the generated optional workspaceTurns capability
Sep 30, 2026
8a058da
Merge origin/main into feat/hosted-bound-later-turns
yiliang114 Sep 30, 2026
f2a028b
test(managed-agent): keep the approval run out of the later-Turn checks
Sep 30, 2026
61ee97e
Merge origin/main into feat/hosted-bound-later-turns
yiliang114 Sep 30, 2026
e96c116
docs(managed-agent): Align G0 gating statements with creator later-Tu…
yiliang114 Sep 30, 2026
2f4abb1
chore(managed-agent): Bump the public API contract to 1.27.0
yiliang114 Sep 30, 2026
af0373c
fix(web-shell): hide execution-unavailable banner line when creator c…
yiliang114 Sep 30, 2026
3a736a3
test(sdk-java): pin bound-Session admission clauses with negative con…
yiliang114 Oct 1, 2026
5d4499c
test(sdk-java): exercise later Turns under approval-mode=default and …
yiliang114 Oct 1, 2026
26de98c
fix(sdk-java): align bound-Session later-Turn admission with the exec…
yiliang114 Oct 1, 2026
f2601d6
Merge origin/main into feat/hosted-bound-later-turns
yiliang114 Oct 1, 2026
dc22300
Merge remote-tracking branch 'origin/main' into feat/hosted-bound-lat…
yiliang114 Oct 1, 2026
66646a6
fix(sdk-java): cancel a live bound Turn through its running attachment
Oct 1, 2026
485c92b
Merge branch 'main' into feat/hosted-bound-later-turns
yiliang114 Oct 1, 2026
9a60cff
fix(managed-agent): stop a bound Turn under refused authorization
Oct 1, 2026
c809439
fix(managed-agent): fail the Turn when a cancel attach is refused
yiliang114 Oct 1, 2026
2431e5e
fix(managed-agent): retire a refused rename command instead of wedgin…
yiliang114 Oct 1, 2026
3f0432b
refactor(managed-agent): drop the dead read-grant operand and fix the…
yiliang114 Oct 1, 2026
ff91ed5
Merge branch 'main' into feat/hosted-bound-later-turns
yiliang114 Oct 1, 2026
31060be
Merge #13112's head into the #13162 follow-up
Oct 1, 2026
f2f8752
refactor(managed-agent): cancel an admitted Turn without attaching
Oct 1, 2026
179aa05
docs(managed-agent): state the full later-Turn admission rule and nar…
yiliang114 Oct 1, 2026
8a26cc9
fix(managed-agent): deliver cancellation through the running owner
yiliang114 Oct 1, 2026
925cffe
fix(ci): bound hosted browser dependency installation
yiliang114 Oct 1, 2026
fa31c02
Merge remote-tracking branch 'origin/main' into codex/ci-browser-inst…
yiliang114 Oct 1, 2026
f6fd035
fix(managed-agent): align cancellation test with runtime recovery
yiliang114 Oct 1, 2026
d857f0f
Merge remote-tracking branch 'origin/main' into codex/pr13163-owner-c…
yiliang114 Oct 1, 2026
77373a0
fix(ci): record the ci.yml growth in the workflow size baseline
yiliang114 Oct 1, 2026
4a79dcf
fix(ci): reserve browser smoke time after slow dependency downloads
yiliang114 Oct 1, 2026
1b5d007
merge(ci): give hosted browser acceptance its full test budget
yiliang114 Oct 1, 2026
ba222e2
fix(ci): keep time for smoke after slow hosted installs
yiliang114 Oct 1, 2026
fd81587
Merge commit 'ba222e2429bd870f49064f5ef5cb5dd50daab6cb' into codex/pr…
yiliang114 Oct 1, 2026
9dffa01
test(web-shell): wait for step expansion before selecting next group
yiliang114 Oct 1, 2026
b73ec01
test(ci): align hosted browser timeout contract
yiliang114 Oct 1, 2026
4ba2b45
Merge commit 'b73ec018e92d202907787ebd146e4717241cdcb2' into codex/pr…
yiliang114 Oct 1, 2026
7b53bba
Merge remote-tracking branch 'origin/main' into feat/hosted-bound-lat…
yiliang114 Oct 2, 2026
e5b2e8c
fix(managed-agent): retry transient cancel-attach refusals instead of…
yiliang114 Oct 2, 2026
43be009
fix(managed-agent): retire the rename command row on every answered f…
yiliang114 Oct 2, 2026
dba017b
docs(managed-agent): qualify Workspace cancel contract
yiliang114 Oct 2, 2026
7720f6d
docs(web-shell): scope Session creation capabilities
yiliang114 Oct 2, 2026
71e9007
docs(managed-agent): state the retired-rename-command contract in the…
yiliang114 Oct 2, 2026
fe91f94
test(managed-agent): pin the command half of requireNoOpenOperation
yiliang114 Oct 2, 2026
7275c03
chore: merge origin/main to refresh the lint gate baseline
yiliang114 Oct 2, 2026
7e5cbdc
fix(ci): sync Hosted later-turn branch with main
yiliang114 Oct 2, 2026
bf1bada
test(web-shell): pin the bound-Session Cancel control at the page level
yiliang114 Oct 2, 2026
f04bf98
fix(managed-agent): answer same-key retries from the record, retire r…
yiliang114 Oct 2, 2026
733e457
fix(managed-agent): clear rename admission when harness is disabled
yiliang114 Oct 2, 2026
220b036
fix(managed-agent): repair the two red required lanes on this head
yiliang114 Oct 2, 2026
bf24730
fix(managed-agent): preserve rename receipts and retry cancellation
yiliang114 Oct 2, 2026
ef77ebf
Merge remote-tracking branch 'origin/main' into feat/hosted-bound-lat…
Oct 2, 2026
b9b4da4
test(managed-agent): expect the closed Session status for a queued bo…
Oct 2, 2026
c3925ff
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 2, 2026
3cd09be
test(managed-agent): read the renamed Session title from metadata
yiliang114 Oct 2, 2026
22a4012
Merge branch 'main' into feat/hosted-bound-later-turns
yiliang114 Oct 2, 2026
9c0bcf4
fix(managed-agent): restore admission error precedence in CI
yiliang114 Oct 2, 2026
03bdd6c
fix(managed-agent): keep the Workspace refusal ahead of the Harness gate
yiliang114 Oct 2, 2026
8953b8f
fix(runtime-broker): close drained lost workspace bindings
yiliang114 Oct 2, 2026
6c5bdee
Merge main into feat/hosted-bound-later-turns
yiliang114 Oct 2, 2026
fb7296c
merge: preserve hosted admission with current workspace retention
yiliang114 Oct 2, 2026
d20a189
merge: retain concurrent PR branch synchronization
yiliang114 Oct 2, 2026
f1387d8
merge: sync cancellation PR with current main
yiliang114 Oct 2, 2026
a745197
fix(managed-agent): retain replay authority while syncing later-turn …
yiliang114 Oct 2, 2026
61ce828
merge: sync refused-authorization cancellation with main
yiliang114 Oct 3, 2026
9cb70b7
Merge branch 'main' into fix/13162-cancel-refused-authorization
yiliang114 Oct 3, 2026
3363fff
docs(managed-agent): state the cancel rule and refusal codes the code…
yiliang114 Oct 3, 2026
2f0d389
fix(managed-agent): answer bound replays only to the creator and keep…
Oct 3, 2026
1701827
fix(managed-agent): keep a retired rename from reverting a newer title
yiliang114 Oct 3, 2026
41cb651
style(web-shell): format the bound Cancel page test with Prettier
Oct 3, 2026
262eb3a
style(web-shell): apply prettier to ManagedSessionsPage.test.tsx
yiliang114 Oct 3, 2026
29e4d17
fix(managed-agent): recover cold-cache cancels without new-work grants
yiliang114 Oct 3, 2026
60030e4
docs(managed-agent): include storage in both capability descriptions
yiliang114 Oct 3, 2026
9fa6b8d
Merge commit 'refs/prheads/13163' into prmerge-13163
yiliang114 Oct 3, 2026
c11d0b5
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 3, 2026
125954a
merge: sync main and preserve cancellation runtime adoption
yiliang114 Oct 3, 2026
52704a9
merge: preserve concurrent PR synchronization
yiliang114 Oct 3, 2026
3683f11
merge: reconcile the unpushed Java admission fix with the live PR head
yiliang114 Oct 3, 2026
8d98903
refactor(managed-agent): delete the review-minted dead surface and it…
yiliang114 Oct 3, 2026
0d4504a
merge: sync main for managed-agent PR
yiliang114 Oct 3, 2026
f8e83fc
fix(managed-agent): dedupe by dropping this PR's own added test, not …
yiliang114 Oct 3, 2026
3c657d6
merge: preserve concurrent PR cleanup
yiliang114 Oct 3, 2026
b27a595
merge: sync PR owner assignment fix
yiliang114 Oct 3, 2026
a18ea0e
fix(serve): retain passive recovery leases on refusal
yiliang114 Oct 4, 2026
762f4a2
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 4, 2026
04b778a
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 4, 2026
288e7fe
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 4, 2026
30f092d
fix(managed-agent): keep the re-registration guard on the page twin
yiliang114 Oct 4, 2026
df8bdc5
fix(managed-agent): keep the Session lifecycle term on cancel admission
yiliang114 Oct 4, 2026
6739048
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 4, 2026
b683a3d
fix(managed-agent): record a passive adoption stranded by a concurren…
yiliang114 Oct 5, 2026
1fbd319
Merge branch 'main' into fix/13162-cancel-refused-authorization
yiliang114 Oct 5, 2026
d210e2d
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 5, 2026
df2547d
Merge branch 'main' into fix/13162-cancel-refused-authorization
yiliang114 Oct 5, 2026
b8f92ce
fix(managed-agent): fence every resident reattach and keep its refusa…
yiliang114 Oct 5, 2026
d71675b
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 5, 2026
b6eff8f
fix(managed-agent): fence the parked passive recovery against teardown
yiliang114 Oct 5, 2026
eb3b933
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 6, 2026
25eb9ae
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 6, 2026
d7aa13a
fix(managed-agent): preserve concurrent recovery and rename ordering
yiliang114 Oct 6, 2026
eeffbb8
Merge main to preserve managed-agent migration order
yiliang114 Oct 6, 2026
6e6da3c
fix(test): import Mockito times in coordinator tests
yiliang114 Oct 6, 2026
13df2a6
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 6, 2026
4b6a2c8
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 7, 2026
684a290
Merge remote-tracking branch 'origin/main' into fix/13162-cancel-refu…
yiliang114 Oct 7, 2026
504ecd9
fix(managed-agent): settle cancelled recovery and refused approvals
yiliang114 Oct 7, 2026
2748c36
chore(repo): retain published PR base synchronization
yiliang114 Oct 7, 2026
f2864f6
fix(serve): preserve live cancellation reattachment
yiliang114 Oct 7, 2026
0227b25
fix(serve): restore resident recovery outcomes
yiliang114 Oct 7, 2026
a9f7fec
fix(managed-agent): retry transient approval mount failures
yiliang114 Oct 7, 2026
3580cd3
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 7, 2026
89aa9d4
fix(managed-agent): renumber the mutation-attempt migration to V51
yiliang114 Oct 7, 2026
3c42bc6
test(managed-agent): expect the V51 mutation-attempt migration in the…
yiliang114 Oct 7, 2026
ce56f43
test(managed-agent): keep Hosted fixture ports distinct
yiliang114 Oct 7, 2026
234037e
fix(managed-agent): retry operator-reversible refusals on the action-…
yiliang114 Oct 8, 2026
2740c23
fix(cli): refuse a resident inapplicable redrive while a prompt slot …
yiliang114 Oct 8, 2026
a4b5452
Merge origin/main into fix/13162-cancel-refused-authorization
yiliang114 Oct 8, 2026
576a39a
test(managed-agent): pin V52 after renumbering the mutation-attempt m…
Oct 8, 2026
00be8ed
fix(managed-agent): preserve approval retries during cold attachment
yiliang114 Oct 8, 2026
f52f123
test(managed-agent): pin the legacy receipt rewrite and the acquire v…
yiliang114 Oct 8, 2026
7867f4b
test(sdk-java): allow heartbeat recovery time after refused detach
yiliang114 Oct 8, 2026
d4cfa63
Merge branch 'main' into fix/13162-cancel-refused-authorization
yiliang114 Oct 8, 2026
0a6ead7
Merge remote-tracking branch 'origin/main' into fix/13162-cancel-refu…
yiliang114 Oct 8, 2026
39267a9
test(hosted): pin teardown and pending rename recovery guards
yiliang114 Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(runtime-broker): close drained lost workspace bindings
  • Loading branch information
yiliang114 committed Oct 2, 2026
commit 8953b8ffb25da9e91b18a2f53b43432fd47580fa
2 changes: 1 addition & 1 deletion docs/design/workspace-session-reliable-close.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Persist a permanent tenant/Harness-Session drain fence under the existing tenant

Fence probes use a nonlocking read after the placement guard, avoiding InnoDB gap locks that would block another tenant's fence insertion when no row exists. Execution admission discovers the immutable binding tenant outside the admission transaction; its first consistent read occurs after acquiring the guard, so REPEATABLE READ cannot hide a fence committed while admission was waiting.

Enumerate saved binding generations by tenant, Session isolation class, and isolation key, in bounded pages with byte-exact identities regardless of database collation. Mark them draining without authorizing current Workspace execution. Enumerate Runtime Sessions by exact binding/generation and release using saved records: provider release, activation=false acknowledgement, conditional original-holder release, then durable RELEASED. No acquire, installation, execution replay, or model call is part of close. Unknown execution or startup identity blocks completion. An unusable original worker blocks with an identity failure instead of entering generic lease recovery. A newer holder on shared storage is preserved.
Enumerate saved binding generations by tenant, Session isolation class, and isolation key, in bounded pages with byte-exact identities regardless of database collation. Mark them draining without authorizing current Workspace execution. Enumerate Runtime Sessions by exact binding/generation and release using saved records: provider release, activation=false acknowledgement, conditional original-holder release, then durable RELEASED. No acquire, installation, execution replay, or model call is part of close. Unknown execution or startup identity blocks completion. A LOST binding with no active Runtime Sessions or executions may retire through the same holder check and durable stop receipt; it remains LOST until the receipt commits RELEASED. A LOST binding with unsettled resources still requires recovery. An unusable original worker with an unreleased Session blocks with an identity failure instead of entering generic lease recovery. A newer holder on shared storage is preserved.

## Worker stop and completion

Expand Down
2 changes: 1 addition & 1 deletion docs/design/workspace-session-reliable-close.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Harness prompt 和既有 Runtime 恢复准入路由在本地 close 开始后拒

栅栏查询在取得 placement 锁后进行普通读取,避免缺失行上的 InnoDB 间隙锁阻塞其他租户插入栅栏。Execution 准入在准入事务之外读取不可变的 binding tenant;事务的首次一致性读发生在取得锁之后,避免 REPEATABLE READ 隐藏等待期间已提交的栅栏。

按 tenant、Session isolation class 和 isolation key 分页枚举保存的 binding 代际;身份按字节精确匹配,不依赖数据库排序规则。标记 draining 时不重新授权当前 Workspace 执行。按精确 binding/generation 枚举 Runtime Sessions,使用保存的记录按顺序释放:provider release、activation=false 确认、条件释放原 holder、持久 RELEASED。close 不进行 acquire、安装、执行重放或模型调用。未知执行或启动身份阻止完成。原 worker 不可用时返回身份失败并阻塞,不进入通用租约恢复。共享存储上的新 holder 必须保留。
按 tenant、Session isolation class 和 isolation key 分页枚举保存的 binding 代际;身份按字节精确匹配,不依赖数据库排序规则。标记 draining 时不重新授权当前 Workspace 执行。按精确 binding/generation 枚举 Runtime Sessions,使用保存的记录按顺序释放:provider release、activation=false 确认、条件释放原 holder、持久 RELEASED。close 不进行 acquire、安装、执行重放或模型调用。未知执行或启动身份阻止完成。没有活跃 Runtime Session 或 execution 的 LOST binding,可以经过同样的 holder 检查与持久停机凭据完成退休;提交凭据并进入 RELEASED 前保持 LOST。有未结算资源的 LOST binding 仍需要恢复。原 worker 不可用且仍有未释放的 Session 时返回身份失败并阻塞,不进入通用租约恢复。共享存储上的新 holder 必须保留。

## Worker 停机与完成

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
Expand Down Expand Up @@ -83,10 +85,11 @@ void ownerAnswersHostedApprovalsThroughBothSurfaces() throws Exception {
assertThat(answered).hasSize(8);
}

@Test
@ParameterizedTest
@ValueSource(booleans = {false, true})
@EnabledOnOs(OS.LINUX)
@Timeout(150)
void durableCloseStopsOriginalWorkersAndRetainsHistoryAndFiles() throws Exception {
void durableCloseStopsOriginalWorkersAndRetainsHistoryAndFiles(boolean crash) throws Exception {
durableClose = true;
runFiles();
List<String> sessions = jdbc.queryForList("SELECT session_id FROM managed_agent_session WHERE tenant_id = ?"
Expand All @@ -101,6 +104,23 @@ void durableCloseStopsOriginalWorkersAndRetainsHistoryAndFiles() throws Exceptio
long pid = json.readTree(Files.readString(registration)).path("pid").asLong();
var worker = ProcessHandle.of(pid).orElseThrow();
assertThat(worker.isAlive()).isTrue();
if (crash) {
worker.destroyForcibly();
worker.onExit().get(5, TimeUnit.SECONDS);
if (index == 1) {
jdbc.update("UPDATE managed_workspace_registry SET config_ref = ? WHERE tenant_id = ?"
+ " AND workspace_id = ?", WorkspaceExecutionProfile.CONFIG_REF, tenant, "workspace-" + index);
String failedTurn = request("POST", "/v1/agents/sessions/" + session + "/events",
Map.of("type", "agent.session.input.message", "input",
List.of(Map.of("type", "input_text", "text", "G0_AGAIN"))),
"after-crash", "actor", 202).path("turn_id").asText();
await().atMost(Duration.ofSeconds(35)).untilAsserted(() -> assertThat(jdbc.queryForObject(
"SELECT status FROM managed_agent_turn WHERE session_id = ? AND turn_id = ?",
String.class, session, failedTurn)).isEqualTo("FAILED"));
}
assertThat(jdbc.queryForObject("SELECT binding_state FROM qwen_runtime_binding WHERE binding_id = ?",
String.class, binding)).isEqualTo(index == 0 ? "READY" : "LOST");
}
var retained = jdbc.queryForList("SELECT resource_id, sha256 FROM qwen_managed_session_resource"
+ " WHERE tenant_id = ? AND session_id = ? ORDER BY resource_id", tenant, session);
assertThat(retained).isNotEmpty();
Expand Down Expand Up @@ -132,6 +152,19 @@ void durableCloseStopsOriginalWorkersAndRetainsHistoryAndFiles() throws Exceptio
.resolve("child/proof.txt"))).isEqualTo("after");
assertThat(request("POST", route, body, "close", "actor", 202)
.path(webShell ? "operationId" : "id").asText()).isEqualTo(operation);
if (crash) {
jdbc.update("UPDATE managed_workspace_registry SET config_ref = ? WHERE tenant_id = ?"
+ " AND workspace_id = ?", WorkspaceExecutionProfile.CONFIG_REF, tenant, "workspace-" + index);
String nextSession = request("POST", "/v1/agents/sessions",
Map.of("agent_id", "qwen-code", "input", List.of(Map.of("type", "input_text", "text", "G0_FILES")),
"workspace", Map.of("workspace_id", "workspace-" + index, "cwd_relative", "child")),
"after-close-" + index, "actor", 202).path("id").asText();
await().atMost(Duration.ofSeconds(35)).untilAsserted(() -> assertThat(jdbc.queryForObject(
"SELECT status FROM managed_agent_turn WHERE session_id = ?", String.class, nextSession))
.isEqualTo("COMPLETED"));
assertThat(Files.readString(temporary.resolve(index == 0 ? "workspace-a" : "workspace-b")
.resolve("child/proof.txt"))).isEqualTo("after");
Comment thread
yiliang114 marked this conversation as resolved.
}
}
}

Expand Down Expand Up @@ -536,7 +569,8 @@ private void modelReply(HttpExchange exchange) throws IOException {
String role = message.path("role").asText();
if ("user".equals(role) && message.path("content").toString().contains("G0_")) {
results.clear();
prompt.set(message.path("content").toString());
String content = message.path("content").toString();
prompt.set(content.substring(content.lastIndexOf("G0_")));
} else if ("tool".equals(role)) {
results.add(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,10 @@ public boolean isWorkspaceFilesAvailable() {
// stays admitted.
assertThat(enabled.getWebShellSession(tenant, "actor-a", controlId)
.capabilities().workspaceTurns()).isTrue();
ManagedAgentService disabled = new ManagedAgentService(store,
new RequestDigests(), null, new UnavailableHarnessConnector(), registry);
assertThat(disabled.getWebShellSession(tenant, "actor-a", controlId)
.capabilities().workspaceTurns()).isFalse();
}

@Test
Expand Down Expand Up @@ -735,6 +739,21 @@ public void rename(String tenantId, String sessionId,
+ " AND session_id = ? AND command_status = 'PENDING'",
Integer.class, tenant, sessionId)).isZero();

transaction.executeWithoutResult(status -> gated.beginSessionMutation(
tenant, "RENAME_SESSION", "rename-blocker", digest, sessionId,
SessionMutationKind.RENAME));
transaction.executeWithoutResult(status ->
assertThatThrownBy(() -> service.renameSession(tenant,
"actor-a", "rename-1", sessionId, "first"))
.isInstanceOfSatisfying(ApiException.class, error ->
assertThat(error.getCode()).isEqualTo("session_operation_active")));
assertThat(jdbc.queryForObject("SELECT command_status FROM managed_agent_command"
+ " WHERE tenant_id = ? AND operation = 'RENAME_SESSION' AND idempotency_key = 'rename-1'",
String.class, tenant)).isEqualTo("FAILED");
transaction.executeWithoutResult(status -> gated.completeSessionMutation(
tenant, "RENAME_SESSION", "rename-blocker", sessionId,
SessionMutationKind.RENAME, "intermediate", "boot"));

// The freed key stays re-usable: a same-key retry re-attempts the
// mutation instead of colliding on the requested event the refused
// attempt already published.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ void requireSafeReplacement(RuntimeBindingRecord replacement) {
&& stopEvidence != null) {
throw new IllegalArgumentException("Recovery evidence cannot be overwritten");
}
if ((state == State.LOST && replacement.state != State.LOST)
if ((state == State.LOST && replacement.state != State.LOST
&& !(replacement.state == State.RELEASED && replacement.drainReceipt != null))
|| state == State.OPERATOR_RECOVERY
&& replacement.state != State.OPERATOR_RECOVERY
&& replacement.state != State.LOST) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ private CompletionStage<Void> drainBinding(RuntimeBindingRecord saved) {
return CompletableFuture.completedFuture(null);
}
if (!saved.getRequest().isManagedContext() || !provisioner.supportsDrainedStop()
|| saved.getState() == RuntimeBindingRecord.State.LOST
|| saved.getState() == RuntimeBindingRecord.State.OPERATOR_RECOVERY
|| saved.getState() == RuntimeBindingRecord.State.FAILED) {
return failed(conflict("workspace_close_identity_unverified", "Original worker needs recovery"));
Expand All @@ -214,8 +213,14 @@ private CompletionStage<Void> drainClaimedBinding(RuntimeBindingRecord saved) {
if (claimed == null) {
return failed(unavailable("runtime_close_claim_pending", "Original binding is still claimed"));
}
boolean lost = claimed.getState() == RuntimeBindingRecord.State.LOST;
if (lost && (sessionRepository.countActiveByBinding(claimed.getBindingId(), claimed.getGeneration()) != 0
|| executionRepository.hasActiveByBinding(claimed.getBindingId(), claimed.getGeneration()))) {
releaseOperationQuietly(claimed.getBindingId(), claimed.getOperationGeneration());
return failed(conflict("workspace_close_execution_unsettled", "Lost Runtime resources require recovery"));
}
var draining = bindingRepository.compareAndSet(claimed, claimed.withDrainRequested(true, clock.instant())
.withState(RuntimeBindingRecord.State.DRAINING,
.withState(lost ? RuntimeBindingRecord.State.LOST : RuntimeBindingRecord.State.DRAINING,
claimed.getLease(), clock.instant()));
if (draining == null) {
releaseOperationQuietly(claimed.getBindingId(), claimed.getOperationGeneration());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ void stalledDrainReleasesItsClaimAndFencesLateCompletion(String step) throws Exc
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void drainingBlocksRivalStoragePlacementUntilStopIsProven(boolean jdbc) throws Exception {
@CsvSource({"false,RECOVERY_BLOCKED", "true,RECOVERY_BLOCKED", "false,LOST", "true,LOST"})
void drainingBlocksRivalStoragePlacementUntilStopIsProven(boolean jdbc, String state) throws Exception {
RuntimeBindingRepository registry = jdbc ? bindings : new InMemoryRuntimeBindingRepository();
var ready = ready(registry);
var claimed = registry.claimOperation(ready.getBindingId(), "block", Duration.ofSeconds(10));
var blocked = registry.compareAndSet(claimed, claimed.withState(RuntimeBindingRecord.State.RECOVERY_BLOCKED,
var blocked = registry.compareAndSet(claimed, claimed.withState(RuntimeBindingRecord.State.valueOf(state),
claimed.getLease(), Instant.now()));
assertNotNull(blocked);
var binding = registry.releaseOperation(blocked.getBindingId(), "block", blocked.getOperationGeneration());
Expand All @@ -203,7 +203,8 @@ void drainingBlocksRivalStoragePlacementUntilStopIsProven(boolean jdbc) throws E
provisioner, transport, registry, sessions, executions, "drainer", Duration.ofSeconds(2), Duration.ofSeconds(2))) {
service.requestHarnessDrain("tenant", "harness");
var close = service.drainHarnessSession("tenant", "harness").toCompletableFuture();
assertEquals(RuntimeBindingRecord.State.DRAINING, registry.findById(binding.getBindingId()).getState());
assertEquals("LOST".equals(state) ? RuntimeBindingRecord.State.LOST : RuntimeBindingRecord.State.DRAINING,
registry.findById(binding.getBindingId()).getState());
assertEquals("runtime_placement_recovery_required",
assertThrows(RuntimeBrokerException.class, () -> registry.findOrCreate(rival)).getCode());
registry.findOrCreate(new RuntimeProvisionRequest(rivalScope, "unrelated", "local-process", "other-storage"));
Expand Down Expand Up @@ -266,6 +267,28 @@ void oldProvisioningReplyCannotLaunchAfterAnotherBrokerRetiresItsIntent() throws
}
}

@Test
void lostBindingWithAnUnreleasedSessionStillRequiresRecovery() throws Exception {
var binding = ready();
var session = bindings.admitSession(sessions, candidate(binding, "one"));
var claimed = bindings.claimOperation(binding.getBindingId(), "setup", Duration.ofSeconds(10));
var lost = bindings.compareAndSet(claimed, claimed.withState(RuntimeBindingRecord.State.LOST,
claimed.getLease(), Instant.now()));
bindings.releaseOperation(lost.getBindingId(), "setup", lost.getOperationGeneration());
try (var service = service()) {
service.requestHarnessDrain("tenant", "harness");
var failure = assertThrows(ExecutionException.class,
() -> service.drainHarnessSession("tenant", "harness").toCompletableFuture().get());
assertEquals("workspace_close_execution_unsettled",
Comment thread
yiliang114 marked this conversation as resolved.
assertInstanceOf(RuntimeBrokerException.class, failure.getCause()).getCode());
assertTrue(sessions.findById(scope, session.getRuntimeSessionId()).isActive());
assertEquals(RuntimeBindingRecord.State.LOST, bindings.findById(binding.getBindingId()).getState());
assertNull(bindings.findById(binding.getBindingId()).getDrainReceipt());
assertEquals(0, transport.releases);
assertEquals(0, provisioner.stops);
}
}

@Test
void unknownExecutionBlocksReleaseAndStop() throws Exception {
var binding = ready();
Expand Down