Skip to content

Commit c6c2aaf

Browse files
authored
Assorted fixes for the nested / docker runtimes. (#8899)
1 parent 7bea93b commit c6c2aaf

File tree

4 files changed

+31
-23
lines changed

4 files changed

+31
-23
lines changed

openhands/runtime/impl/docker/docker_runtime.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -374,28 +374,11 @@ def init_container(self) -> None:
374374
)
375375
self.log('debug', f'Container started. Server url: {self.api_url}')
376376
self.send_status_message('STATUS$CONTAINER_STARTED')
377-
except docker.errors.APIError as e:
378-
if '409' in str(e):
379-
self.log(
380-
'warning',
381-
f'Container {self.container_name} already exists. Removing...',
382-
)
383-
stop_all_containers(self.container_name)
384-
return self.init_container()
385-
386-
else:
387-
self.log(
388-
'error',
389-
f'Error: Instance {self.container_name} FAILED to start container!\n',
390-
)
391-
self.log('error', str(e))
392-
raise e
393377
except Exception as e:
394378
self.log(
395379
'error',
396380
f'Error: Instance {self.container_name} FAILED to start container!\n',
397381
)
398-
self.log('error', str(e))
399382
self.close()
400383
raise e
401384

openhands/server/conversation_manager/docker_nested_conversation_manager.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class DockerNestedConversationManager(ConversationManager):
5454
docker_client: docker.DockerClient = field(default_factory=docker.from_env)
5555
_conversation_store_class: type[ConversationStore] | None = None
5656
_starting_conversation_ids: set[str] = field(default_factory=set)
57+
_runtime_container_image: str | None = None
5758

5859
async def __aenter__(self):
5960
# No action is required on startup for this implementation
@@ -155,6 +156,12 @@ async def _start_agent_loop(
155156
try:
156157
# Build the runtime container image if it is missing
157158
await call_sync_from_async(runtime.maybe_build_runtime_container_image)
159+
self._runtime_container_image = runtime.runtime_container_image
160+
161+
# check that the container already exists...
162+
if await self._start_existing_container(runtime):
163+
self._starting_conversation_ids.discard(sid)
164+
return
158165

159166
# initialize the container but dont wait for it to start
160167
await call_sync_from_async(runtime.init_container)
@@ -172,7 +179,7 @@ async def _start_agent_loop(
172179
)
173180

174181
except Exception:
175-
self._starting_conversation_ids.remove(sid)
182+
self._starting_conversation_ids.discard(sid)
176183
raise
177184

178185
async def _start_conversation(
@@ -262,7 +269,7 @@ async def _start_conversation(
262269
)
263270
assert response.status_code == status.HTTP_200_OK
264271
finally:
265-
self._starting_conversation_ids.remove(sid)
272+
self._starting_conversation_ids.discard(sid)
266273

267274
async def send_to_event_stream(self, connection_id: str, data: dict):
268275
# Not supported - clients should connect directly to the nested server!
@@ -431,6 +438,7 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
431438
# We need to be able to specify the nested conversation id within the nested runtime
432439
env_vars['ALLOW_SET_CONVERSATION_ID'] = '1'
433440
env_vars['WORKSPACE_BASE'] = f'/workspace'
441+
env_vars['SANDBOX_CLOSE_DELAY'] = '0'
434442

435443
# Set up mounted volume for conversation directory within workspace
436444
# TODO: Check if we are using the standard event store and file store
@@ -442,9 +450,11 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
442450
conversation_dir = get_conversation_dir(sid, user_id)
443451

444452
volumes.append(
445-
f'{config.file_store_path}/{conversation_dir}:/root/openhands/file_store/{conversation_dir}:rw'
453+
f'{config.file_store_path}/{conversation_dir}:/root/.openhands/file_store/{conversation_dir}:rw'
446454
)
447455
config.sandbox.volumes = ','.join(volumes)
456+
if not config.sandbox.runtime_container_image:
457+
config.sandbox.runtime_container_image = self._runtime_container_image
448458

449459
# Currently this eventstream is never used and only exists because one is required in order to create a docker runtime
450460
event_stream = EventStream(sid, self.file_store, user_id)
@@ -464,6 +474,18 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
464474

465475
return runtime
466476

477+
async def _start_existing_container(self, runtime: DockerRuntime) -> bool:
478+
try:
479+
container = self.docker_client.containers.get(runtime.container_name)
480+
if container:
481+
status = container.status
482+
if status == 'exited':
483+
await call_sync_from_async(container.start())
484+
return True
485+
return False
486+
except docker.errors.NotFound as e:
487+
return False
488+
467489

468490
def _last_updated_at_key(conversation: ConversationMetadata) -> float:
469491
last_updated_at = conversation.last_updated_at

openhands/server/conversation_manager/standalone_conversation_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ async def _cleanup_stale(self):
154154
await conversation.disconnect()
155155
self._detached_conversations.pop(sid, None)
156156

157+
# Implies disconnected sandboxes stay open indefinitely
158+
if not self.config.sandbox.close_delay:
159+
return
160+
157161
close_threshold = time.time() - self.config.sandbox.close_delay
158162
running_loops = list(self._local_agent_loops_by_sid.items())
159163
running_loops.sort(key=lambda item: item[1].last_active_ts)

openhands/server/utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import Depends, Request
22

33
from openhands.server.shared import ConversationStoreImpl, config, conversation_manager
4-
from openhands.server.user_auth import get_user_auth, get_user_id
4+
from openhands.server.user_auth import get_user_id
55
from openhands.storage.conversation.conversation_store import ConversationStore
66

77

@@ -11,8 +11,7 @@ async def get_conversation_store(request: Request) -> ConversationStore | None:
1111
)
1212
if conversation_store:
1313
return conversation_store
14-
user_auth = await get_user_auth(request)
15-
user_id = await user_auth.get_user_id()
14+
user_id = get_user_id(request)
1615
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
1716
request.state.conversation_store = conversation_store
1817
return conversation_store

0 commit comments

Comments
 (0)