Skip to content

Commit 7a6596a

Browse files
committed
Continue accepting processes as long as the agent is active
1 parent 7a8516e commit 7a6596a

2 files changed

Lines changed: 22 additions & 111 deletions

File tree

src/main.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ async fn maybe_run_until_idle(args: &argv::Args, procs: &SharedProcs) {
105105
}
106106

107107
if args.agent {
108-
// For --wait --agent: wait for exactly one run to be assigned, then disconnect
108+
// For --wait --agent: wait for at least one process to be assigned,
109+
// then wait for all processes to be deleted before disconnecting
109110
let mut sub = procs.subscribe();
110111

111-
// If already has work, skip waiting
112+
// If already has work, skip waiting for first assignment
112113
if procs.is_empty() {
113114
// Wait for first process assignment
114115
loop {
@@ -125,15 +126,13 @@ async fn maybe_run_until_idle(args: &argv::Args, procs: &SharedProcs) {
125126
}
126127
}
127128

128-
// Process has been assigned! Set shutdown to Idling to prevent accepting other processes
129-
procs.set_shutdown(shutdown::State::Idling);
130-
131-
// Now wait for the single assigned process to complete
129+
// At least one process has been assigned! Continue accepting processes
130+
// and only shutdown when all assigned processes are deleted
132131
tokio::select! {
133132
_ = procs.wait_idle() => {},
134133
_ = procs.wait_for_shutdown() => {},
135134
};
136-
// Process completed and deleted, agent should disconnect
135+
procs.set_shutdown(shutdown::State::Done);
137136
} else {
138137
// Non-agent mode: just wait until idle then exit
139138
tokio::select! {

tests/int/agent/test_wait_mode.py

Lines changed: 16 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -24,49 +24,8 @@ async def test_agent_selection_logic():
2424
assert conn.shutdown_state.name == "active"
2525

2626

27-
@pytest.mark.asyncio
28-
async def test_agent_rejects_second_run():
29-
"""
30-
Test that after accepting one run, agent becomes unavailable for more runs.
31-
"""
32-
async with Assembly.start(args=["--wait"]) as asm:
33-
conn = next(iter(asm.server.connections.values()))
34-
35-
# Start first process
36-
proc_spec1 = spec.Proc(["/bin/sleep", "1"])
37-
proc1, result1 = await asm.server.start(
38-
proc_id="test-proc-1", group_id="default", spec=proc_spec1
39-
)
40-
41-
# Give time for agent to set shutdown state to idling
42-
await asyncio.sleep(0.15)
43-
44-
# Agent should now be in idling state, making it unavailable for new work
45-
conn = asm.server.connections.get(conn.info.conn.conn_id)
46-
assert conn.shutdown_state.name in ["idling"]
47-
48-
# Attempt to start second process should fail due to no available connections
49-
proc_spec2 = spec.Proc(["/bin/echo", "second"])
50-
51-
with pytest.raises(NoOpenConnectionInGroup):
52-
await asm.server.start(
53-
proc_id="test-proc-2",
54-
group_id="default",
55-
spec=proc_spec2,
56-
conn_timeout=0.5, # Short timeout
57-
)
58-
59-
# Wait for first process to complete
60-
async for update in proc1.updates:
61-
if isinstance(update, Result) and update.state != "running":
62-
break
63-
64-
6527
@pytest.mark.asyncio
6628
async def test_agent_state_transitions():
67-
"""
68-
Test the exact state transitions: active -> idling -> done.
69-
"""
7029
async with Assembly.start(args=["--wait"]) as asm:
7130
conn = next(iter(asm.server.connections.values()))
7231
assert len(asm.server.connections) >= 1
@@ -75,91 +34,44 @@ async def test_agent_state_transitions():
7534
)
7635

7736
await asyncio.sleep(1)
78-
# agent stays active when no work is assigned
37+
# agent stays active until no work is assigned
7938
assert conn.shutdown_state.name == "active", (
80-
f"Agent should remain active when idle, got: {conn.shutdown_state.name}"
39+
f"Agent should remain active until some work is assigned, got: {conn.shutdown_state.name}"
8140
)
8241

83-
# Initially active
84-
assert conn.shutdown_state.name == "active"
85-
8642
# Start a process that runs for a short time
8743
proc_spec = spec.Proc(["/bin/sleep", "0.2"])
8844
proc, result = await asm.server.start(
8945
proc_id="test-proc", group_id="default", spec=proc_spec
9046
)
9147

92-
# Give time for state transition to idling
93-
await asyncio.sleep(0.15)
94-
95-
conn_id = conn.info.conn.conn_id
96-
# Should now be idling (prevents new work)
97-
conn = asm.server.connections.get(conn_id)
98-
if conn:
99-
assert conn.shutdown_state.name == "idling"
100-
101-
# Wait for process to complete
10248
async for update in proc.updates:
10349
if isinstance(update, Result) and update.state != "running":
10450
break
10551

106-
# Give time for final state transition and cleanup
107-
await asyncio.sleep(0.3)
108-
109-
# Connection should be done or cleaned up
52+
conn_id = conn.info.conn.conn_id
11053
conn = asm.server.connections.get(conn_id)
111-
if conn:
112-
# Agent might still be connected but should be done or idling (about to disconnect)
113-
assert conn.shutdown_state.name in ["done", "idling"]
114-
115-
116-
@pytest.mark.asyncio
117-
async def test_multiple_agents():
118-
"""
119-
Test multiple single-run agents can each accept one run.
120-
"""
121-
async with Assembly.start(counts={"default": 3}, args=["--wait"]) as asm:
122-
assert len(asm.server.connections) == 3
123-
124-
for conn in asm.server.connections.values():
125-
assert conn.shutdown_state.name == "active"
126-
127-
# Start 3 processes - each should go to a different agent
128-
procs = []
129-
for i in range(3):
130-
proc_spec = spec.Proc(["/bin/echo", f"task-{i}"])
131-
proc, result = await asm.server.start(
132-
proc_id=f"test-proc-{i}", group_id="default", spec=proc_spec
133-
)
134-
procs.append(proc)
135-
# Give time for agent state changes to propagate
136-
await asyncio.sleep(0.2)
54+
assert conn.shutdown_state.name == "active", (
55+
"Agent should stay active until all processes are deleted"
56+
)
13757

138-
# Give time for state transitions
139-
await asyncio.sleep(0.8)
58+
# Delete the completed process to trigger the shutdown sequence
59+
await proc.delete()
14060

141-
# All agents should now be idling (no longer accepting work)
142-
active_count = sum(
143-
1 for conn in asm.server.connections.values() if conn.shutdown_state.name == "active"
144-
)
145-
assert active_count == 0, "All agents should have transitioned from active state"
61+
await asyncio.sleep(1)
14662

147-
# Wait for all processes to complete
148-
for proc in procs:
149-
async for update in proc.updates:
150-
if isinstance(update, Result) and update.state != "running":
151-
break
63+
active_conns = asm.server.connections._get_open_conns_in_group("default")
64+
assert len(active_conns) == 0, f"Expected no active connections, got {len(active_conns)}"
15265

153-
# Give time for cleanup
154-
await asyncio.sleep(0.1)
66+
# Attempt to start second process should fail due to no available connections
67+
proc_spec2 = spec.Proc(["/bin/echo", "second"])
15568

15669
with pytest.raises(NoOpenConnectionInGroup):
157-
proc_spec = spec.Proc(["/bin/echo", "should-fail"])
15870
await asm.server.start(
159-
proc_id="test-proc-fail",
71+
proc_id="test-proc-2",
16072
group_id="default",
161-
spec=proc_spec,
162-
conn_timeout=0.1,
73+
spec=proc_spec2,
74+
conn_timeout=0.5,
16375
)
16476

16577

0 commit comments

Comments
 (0)