Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
PlannerApiError,
PlannerClient,
PlannerConfiguration,
PlannerConnectionError,
)
from github_runner_manager.platform.github_provider import GitHubRunnerPlatform

Expand Down Expand Up @@ -131,14 +132,23 @@ def start_create_loop(self) -> None:
if self._stop.is_set():
return
self._handle_create_runners(update.pressure)
except PlannerConnectionError as exc:
fallback = max(self._last_pressure or 0, self._config.min_pressure)
logger.warning(
"Pressure stream interrupted (%s), falling back to %s runners.",
exc,
fallback,
)
Comment thread
cbartz marked this conversation as resolved.
self._handle_create_runners(fallback)
self._stop.wait(5)
Comment thread
cbartz marked this conversation as resolved.
Comment thread
cbartz marked this conversation as resolved.
Comment thread
cbartz marked this conversation as resolved.
except PlannerApiError:
fallback = max(self._last_pressure or 0, self._config.min_pressure)
logger.exception(
"Error in pressure stream loop, falling back to %s runners.",
fallback,
)
Comment thread
cbartz marked this conversation as resolved.
self._handle_create_runners(fallback)
time.sleep(5)
self._stop.wait(5)
Comment thread
cbartz marked this conversation as resolved.
Comment thread
cbartz marked this conversation as resolved.

def start_reconcile_loop(self) -> None:
"""Periodically reconcile runners: sync state, scale up/down, and clean up."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class PlannerApiError(Exception):
"""Represents an error while interacting with the planner service."""


class PlannerConnectionError(PlannerApiError):
"""Transient connection error (dropped stream, timeout)."""

Comment thread
cbartz marked this conversation as resolved.

class PlannerClient: # pylint: disable=too-few-public-methods
"""An HTTP client for the planner service."""

Expand Down Expand Up @@ -93,9 +97,10 @@ def stream_pressure(self, name: str) -> Iterable[PressureInfo]:
except json.JSONDecodeError:
logger.warning("Skipping malformed stream line: %s", line)
continue
except (requests.ConnectionError, requests.Timeout) as exc:
raise PlannerConnectionError(str(exc)) from exc
except requests.RequestException as exc:
logger.exception("Error while streaming pressure for flavor '%s' from planner.", name)
raise PlannerApiError from exc
raise PlannerApiError(str(exc)) from exc
Comment thread
cbartz marked this conversation as resolved.
Outdated
Comment thread
cbartz marked this conversation as resolved.

@staticmethod
def _create_session() -> requests.Session:
Expand Down
54 changes: 54 additions & 0 deletions github-runner-manager/tests/unit/test_planner_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import json
from types import SimpleNamespace

import pytest
import requests

from github_runner_manager.planner_client import (
PlannerApiError,
PlannerClient,
PlannerConfiguration,
PlannerConnectionError,
)


Expand Down Expand Up @@ -146,3 +149,54 @@ def test_stream_pressure_success(monkeypatch):
updates = list(client.stream_pressure("small"))
assert updates[0].pressure == 2
assert updates[1].pressure == 5


@pytest.mark.parametrize(
("request_error", "message"),
[
(requests.ConnectionError, "connection dropped"),
(requests.Timeout, "timed out"),
],
)
def test_stream_pressure_raises_connection_error_on_transient_failures(
monkeypatch, request_error, message
):
"""
arrange: Fake session raises a transient requests error.
act: Consume `stream_pressure('small')`.
assert: Raises `PlannerConnectionError`.
"""
cfg = PlannerConfiguration(base_url="http://localhost:8080", token="t")
client = PlannerClient(cfg)

fake_session = _FakeSession()
monkeypatch.setattr(client, "_session", fake_session)

def _raise_transient_error(url, headers, timeout, stream=False):
raise request_error(message)

monkeypatch.setattr(fake_session, "get", _raise_transient_error)

with pytest.raises(PlannerConnectionError, match=message):
next(client.stream_pressure("small"))


def test_stream_pressure_raises_planner_api_error_on_request_exception(monkeypatch):
"""
arrange: Fake session raises a non-transient `requests.RequestException`.
act: Consume `stream_pressure('small')`.
assert: Raises `PlannerApiError`.
"""
cfg = PlannerConfiguration(base_url="http://localhost:8080", token="t")
client = PlannerClient(cfg)

fake_session = _FakeSession()
monkeypatch.setattr(client, "_session", fake_session)

def _raise_request_exception(url, headers, timeout, stream=False):
raise requests.RequestException("request failed")

monkeypatch.setattr(fake_session, "get", _raise_request_exception)

with pytest.raises(PlannerApiError, match="request failed"):
next(client.stream_pressure("small"))
Loading