mirror of
https://github.com/run-llama/workflows-py.git
synced 2026-07-20 19:57:02 -04:00
llamactl: deployments update — single event loop across get + update (#602)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"llamactl": patch
|
||||
---
|
||||
|
||||
Fix `deployments update` crashing with `Event loop is closed` after a transient failure on the internal git push. The command now runs `get_deployment` and `update_deployment` in a single event loop instead of reusing the same `ProjectClient` across two `asyncio.run` calls.
|
||||
@@ -331,8 +331,9 @@ def edit_deployment(
|
||||
|
||||
def _push_internal_for_update(
|
||||
deployment_id: str,
|
||||
git_ref: str | None = None,
|
||||
project_id_override: str | None = None,
|
||||
git_ref: str | None,
|
||||
base_url: str,
|
||||
project_id: str,
|
||||
) -> None:
|
||||
"""Push local code to the internal repo before updating.
|
||||
|
||||
@@ -348,12 +349,9 @@ def _push_internal_for_update(
|
||||
)
|
||||
return
|
||||
|
||||
client = get_project_client(project_id_override=project_id_override)
|
||||
api_key = get_api_key()
|
||||
git_url = get_deployment_git_url(client.base_url, deployment_id)
|
||||
remote_name = configure_git_remote(
|
||||
git_url, api_key, client.project_id, deployment_id
|
||||
)
|
||||
git_url = get_deployment_git_url(base_url, deployment_id)
|
||||
remote_name = configure_git_remote(git_url, api_key, project_id, deployment_id)
|
||||
local_ref, target_ref = internal_push_refspec(git_ref)
|
||||
with console.status("Pushing code..."):
|
||||
push_result = push_to_remote(
|
||||
@@ -394,28 +392,31 @@ def refresh_deployment(
|
||||
rprint(f"[{WARNING}]No deployment selected[/]")
|
||||
return
|
||||
|
||||
client = get_project_client(project_id_override=project)
|
||||
current_deployment = asyncio.run(client.get_deployment(deployment_id))
|
||||
deployment_name = current_deployment.display_name
|
||||
# Single asyncio.run with one client: reusing a ProjectClient across
|
||||
# two asyncio.run calls binds the underlying httpx pool to a closed
|
||||
# loop and the next request raises "Event loop is closed".
|
||||
async def _do_update() -> tuple[DeploymentResponse, DeploymentResponse]:
|
||||
async with project_client_context(project_id_override=project) as client:
|
||||
current = await client.get_deployment(deployment_id)
|
||||
effective_git_ref = git_ref or current.git_ref
|
||||
if current.repo_url == INTERNAL_CODE_REPO_SCHEME:
|
||||
_push_internal_for_update(
|
||||
deployment_id,
|
||||
effective_git_ref,
|
||||
base_url=client.base_url,
|
||||
project_id=client.project_id,
|
||||
)
|
||||
# Re-resolves the branch to the latest commit SHA on the server.
|
||||
with console.status(f"Refreshing {current.display_name}..."):
|
||||
updated = await client.update_deployment(
|
||||
deployment_id,
|
||||
DeploymentUpdate(git_ref=effective_git_ref),
|
||||
)
|
||||
return current, updated
|
||||
|
||||
current_deployment, updated_deployment = asyncio.run(_do_update())
|
||||
|
||||
old_git_sha = current_deployment.git_sha or ""
|
||||
|
||||
# For internal repos, push local code first so the server has the
|
||||
# latest commits to resolve against.
|
||||
effective_git_ref = git_ref or current_deployment.git_ref
|
||||
if current_deployment.repo_url == INTERNAL_CODE_REPO_SCHEME:
|
||||
_push_internal_for_update(
|
||||
deployment_id, effective_git_ref, project_id_override=project
|
||||
)
|
||||
|
||||
# Re-resolves the branch to the latest commit SHA.
|
||||
with console.status(f"Refreshing {deployment_name}..."):
|
||||
updated_deployment = asyncio.run(
|
||||
client.update_deployment(
|
||||
deployment_id,
|
||||
DeploymentUpdate(git_ref=effective_git_ref),
|
||||
)
|
||||
)
|
||||
|
||||
new_git_sha = updated_deployment.git_sha or ""
|
||||
old_short = short_sha(old_git_sha) if old_git_sha else "-"
|
||||
new_short = short_sha(new_git_sha) if new_git_sha else "-"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2026 LlamaIndex Inc.
|
||||
"""Tests for ``deployments update``.
|
||||
|
||||
The headline regression here: ``deployments update <id>`` for an internal-code
|
||||
deployment used to call ``asyncio.run`` twice with the same ``ProjectClient``.
|
||||
The shared httpx pool was bound to the first (closed) event loop, and the
|
||||
second run raised ``RuntimeError: Event loop is closed`` — surfacing in CI as
|
||||
``Error: Event loop is closed`` after the "Continuing with update using last
|
||||
pushed code" fallback when the internal git mirror returned a transient 500.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
from conftest import make_deployment, patch_project_client
|
||||
from llama_agents.cli.app import app
|
||||
from llama_agents.core.schema.deployments import (
|
||||
INTERNAL_CODE_REPO_SCHEME,
|
||||
DeploymentResponse,
|
||||
)
|
||||
|
||||
|
||||
def _client_mock(current: DeploymentResponse, updated: DeploymentResponse) -> MagicMock:
|
||||
"""ProjectClient stand-in for the update flow."""
|
||||
|
||||
async def _get(
|
||||
deployment_id: str, include_events: bool = False
|
||||
) -> DeploymentResponse:
|
||||
return current
|
||||
|
||||
async def _update(deployment_id: str, update: Any) -> DeploymentResponse:
|
||||
return updated
|
||||
|
||||
client = MagicMock()
|
||||
client.get_deployment.side_effect = _get
|
||||
client.update_deployment.side_effect = _update
|
||||
client.project_id = "proj_default"
|
||||
client.base_url = "http://test:8011"
|
||||
return client
|
||||
|
||||
|
||||
def _completed_process(
|
||||
returncode: int = 0, stdout: bytes = b"", stderr: bytes = b""
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
return subprocess.CompletedProcess(
|
||||
args=[], returncode=returncode, stdout=stdout, stderr=stderr
|
||||
)
|
||||
|
||||
|
||||
def test_deployments_update_external_repo(patched_auth: Any) -> None:
|
||||
"""External-repo update: no push step, single asyncio.run, exit 0."""
|
||||
runner = CliRunner()
|
||||
current = make_deployment("my-app", git_sha="a" * 40)
|
||||
updated = make_deployment("my-app", git_sha="b" * 40)
|
||||
client = _client_mock(current, updated)
|
||||
with patch_project_client(client):
|
||||
result = runner.invoke(
|
||||
app, ["deployments", "update", "my-app", "--no-interactive"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
client.get_deployment.assert_called_once()
|
||||
client.update_deployment.assert_called_once()
|
||||
|
||||
|
||||
def test_deployments_update_internal_repo_push_failure_does_not_abort(
|
||||
patched_auth: Any,
|
||||
) -> None:
|
||||
"""Push 500 → fallback path runs, deployment still updates, exit 0.
|
||||
|
||||
Reproduces the GitHub Actions failure mode where a transient 500 from the
|
||||
internal git mirror caused ``Event loop is closed`` and a non-zero exit.
|
||||
The fix routes both ``get_deployment`` and ``update_deployment`` through a
|
||||
single ``asyncio.run`` so the httpx pool isn't reused across loops.
|
||||
|
||||
Patches are at the helper boundary (``configure_git_remote`` /
|
||||
``push_to_remote``) so the test doesn't depend on the exact shape of the
|
||||
underlying git plumbing.
|
||||
"""
|
||||
runner = CliRunner()
|
||||
current = make_deployment(
|
||||
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="a" * 40
|
||||
)
|
||||
updated = make_deployment(
|
||||
"my-app", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha="b" * 40
|
||||
)
|
||||
client = _client_mock(current, updated)
|
||||
|
||||
with (
|
||||
patch_project_client(client),
|
||||
# ``git rev-parse --git-dir`` succeeds — we're "in a repo".
|
||||
patch(
|
||||
"llama_agents.cli.commands.deployment.subprocess.run",
|
||||
return_value=_completed_process(returncode=0, stdout=b".git\n"),
|
||||
),
|
||||
patch(
|
||||
"llama_agents.cli.commands.deployment.configure_git_remote",
|
||||
return_value="llamaagents-my-app",
|
||||
),
|
||||
# The push itself fails with a 500 (the originally reported flake).
|
||||
patch(
|
||||
"llama_agents.cli.commands.deployment.push_to_remote",
|
||||
return_value=_completed_process(
|
||||
returncode=1,
|
||||
stderr=(
|
||||
b"error: RPC failed; HTTP 500\n"
|
||||
b"fatal: the remote end hung up unexpectedly\n"
|
||||
),
|
||||
),
|
||||
),
|
||||
):
|
||||
result = runner.invoke(
|
||||
app, ["deployments", "update", "my-app", "--no-interactive"]
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Event loop is closed" not in result.output
|
||||
client.get_deployment.assert_called_once()
|
||||
client.update_deployment.assert_called_once()
|
||||
Reference in New Issue
Block a user