-
Notifications
You must be signed in to change notification settings - Fork 25
feat(pressure reconciler): Support no planner integration #759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ddd61be
304c684
afc63d7
b174387
6d882a8
16a5c56
60c23ce
f8dd50a
5b4b307
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,7 +112,7 @@ class PressureReconciler: # pylint: disable=too-few-public-methods,too-many-ins | |
| def __init__( | ||
| self, | ||
| manager: RunnerManager, | ||
| planner_client: PlannerClient, | ||
| planner_client: PlannerClient | None, | ||
| config: PressureReconcilerConfig, | ||
| lock: Lock, | ||
| ) -> None: | ||
|
|
@@ -122,6 +122,7 @@ def __init__( | |
| manager: Runner manager interface for creating, cleaning up, | ||
| and listing runners. | ||
| planner_client: Client used to stream pressure updates. | ||
| None when no planner relation is configured. | ||
| config: Reconciler configuration. | ||
| lock: Shared lock to serialize operations with other reconcile loops. | ||
| """ | ||
|
|
@@ -140,6 +141,15 @@ def start_create_loop(self) -> None: | |
| with self._lock: | ||
| self._runner_count = len(self._manager.get_runners()) | ||
| logger.info("Create loop: initial sync, _runner_count=%s", self._runner_count) | ||
| if self._planner is None: | ||
| self._last_pressure = self._config.min_pressure | ||
| logger.info( | ||
| "Create loop: no planner configured, using min_pressure=%s", | ||
| self._config.min_pressure, | ||
| ) | ||
cbartz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self._handle_create_runners(self._config.min_pressure) | ||
| self._stop.wait() | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
| return | ||
| while not self._stop.is_set(): | ||
| try: | ||
| for update in self._planner.stream_pressure(self._config.flavor_name): | ||
|
|
@@ -394,31 +404,38 @@ def _desired_total_from_pressure(self, pressure: int) -> int: | |
| return total | ||
|
|
||
|
|
||
| def build_pressure_reconciler(config: ApplicationConfiguration, lock: Lock) -> PressureReconciler: | ||
| def build_pressure_reconciler( | ||
| config: ApplicationConfiguration, manager: RunnerManager, lock: Lock | ||
| ) -> PressureReconciler: | ||
| """Construct a PressureReconciler from application configuration. | ||
|
|
||
| Args: | ||
| config: Application configuration. | ||
| manager: The runner manager to use for creating, cleaning up, and listing runners. | ||
| lock: Shared lock to serialize operations with other reconcile loops. | ||
|
|
||
| Raises: | ||
| ValueError: If no non-reactive combinations are configured. | ||
| ValueError: If planner configuration is partial (only one of URL/token set). | ||
|
|
||
| Returns: | ||
| A fully constructed PressureReconciler. | ||
| """ | ||
| combinations = config.non_reactive_configuration.combinations | ||
| if not combinations: | ||
| first = config.non_reactive_configuration.combinations[0] | ||
| planner_client: PlannerClient | None = None | ||
cbartz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| has_url = bool(config.planner_url) | ||
| has_token = bool(config.planner_token) | ||
| if has_url != has_token: | ||
| raise ValueError( | ||
| "Cannot build PressureReconciler: no non-reactive combinations configured." | ||
| "Partial planner configuration: both planner_url and planner_token must be set" | ||
| " or both unset." | ||
| ) | ||
| if has_url and has_token: | ||
| planner_client = PlannerClient( | ||
| PlannerConfiguration(base_url=config.planner_url, token=config.planner_token) | ||
| ) | ||
| first = combinations[0] | ||
| manager = _build_runner_manager(config, first) | ||
| return PressureReconciler( | ||
| manager=manager, | ||
| planner_client=PlannerClient( | ||
| PlannerConfiguration(base_url=config.planner_url, token=config.planner_token) | ||
| ), | ||
| planner_client=planner_client, | ||
| config=PressureReconcilerConfig( | ||
| flavor_name=config.name, | ||
| reconcile_interval=config.reconcile_interval, | ||
|
|
@@ -429,7 +446,7 @@ def build_pressure_reconciler(config: ApplicationConfiguration, lock: Lock) -> P | |
| ) | ||
|
|
||
|
|
||
| def _build_runner_manager( | ||
| def build_runner_manager( | ||
| config: ApplicationConfiguration, combination: NonReactiveCombination | ||
| ) -> RunnerManager: | ||
| """Build a RunnerManager from application config and a flavor/image combination. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| # Copyright 2026 Canonical Ltd. | ||
| # See LICENSE file for licensing details. | ||
|
|
||
| # RunnerInfo is duplicated in runner_scaler (legacy), will be removed in follow-up PR. | ||
| # pylint: disable=duplicate-code | ||
| """Module for managing the GitHub self-hosted runners hosted on cloud instances.""" | ||
|
|
||
| import copy | ||
|
|
@@ -45,6 +47,27 @@ | |
| IssuedMetricEventsStats = dict[Type[metric_events.Event], int] | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class RunnerInfo: | ||
| """Aggregated information on the runners. | ||
|
|
||
| Attributes: | ||
| online: The number of runners in online state. | ||
| busy: The number of runners in busy state. | ||
| offline: The number of runners in offline state. | ||
| unknown: The number of runners in unknown state. | ||
| runners: The names of the online runners. | ||
| busy_runners: The names of the busy runners. | ||
| """ | ||
|
|
||
| online: int | ||
| busy: int | ||
| offline: int | ||
| unknown: int | ||
| runners: tuple[str, ...] | ||
| busy_runners: tuple[str, ...] | ||
|
|
||
|
|
||
| class FlushMode(Enum): | ||
| """Strategy for flushing runners. | ||
|
|
||
|
|
@@ -263,6 +286,42 @@ def get_runners(self) -> tuple[RunnerInstance, ...]: | |
| for vm in vms | ||
| ) | ||
|
|
||
| def get_runner_info(self) -> RunnerInfo: | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. runner_scaler will be removed in a follow-up PR. The logic seems appropriate to live in runner_manager |
||
| """Get aggregated information on the runners. | ||
|
|
||
| Returns: | ||
| Aggregated runner counts and names. | ||
| """ | ||
| runner_list = self.get_runners() | ||
| online = 0 | ||
| busy = 0 | ||
| offline = 0 | ||
| unknown = 0 | ||
| online_runners: list[str] = [] | ||
| busy_runners: list[str] = [] | ||
| for runner in runner_list: | ||
| match runner.platform_state: | ||
| case PlatformRunnerState.BUSY: | ||
| online += 1 | ||
| online_runners.append(runner.name) | ||
| busy += 1 | ||
| busy_runners.append(runner.name) | ||
| case PlatformRunnerState.IDLE: | ||
| online += 1 | ||
| online_runners.append(runner.name) | ||
| case PlatformRunnerState.OFFLINE: | ||
| offline += 1 | ||
| case _: | ||
| unknown += 1 | ||
| return RunnerInfo( | ||
| online=online, | ||
| busy=busy, | ||
| offline=offline, | ||
| unknown=unknown, | ||
| runners=tuple(online_runners), | ||
| busy_runners=tuple(busy_runners), | ||
| ) | ||
|
|
||
| def delete_runners(self, num: int) -> IssuedMetricEventsStats: | ||
| """Delete up to `num` runners, preferring idle ones over busy. | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.