Repository navigation
Add component tests for controller logic #81
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
Open
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # Copyright (c) 2026 Nutanix, Inc. All rights reserved. | ||
| # | ||
| # Author: Leonardo Forchini <leonardo.forchini@nutanix.com> | ||
|
|
||
| """Controller policy observed through a running daemon and the fake backend.""" | ||
|
|
||
| import time | ||
|
|
||
| from harness.common import POLL_S, wait_for | ||
|
|
||
| HEALTHY_VM = "vm-a" | ||
| FAILED_VM = "vm-bad" | ||
|
|
||
| def _stat(user, idle): | ||
| """Render a minimal ``/proc/stat`` with the given user and idle jiffies.""" | ||
| body = "cpu %d 0 0 %d 0 0 0 0 0 0\n" % (user, idle) | ||
| body += "cpu0 %d 0 0 %d 0 0 0 0 0 0\n" % (user, idle) | ||
| body += "ctxt 1\nbtime 1\nprocesses 1\n" | ||
| return body.encode("ascii") | ||
|
|
||
|
|
||
| def _high_host_sequence(): | ||
| """Baseline sample, then busy deltas that pin host utilisation near 1.""" | ||
| samples = [_stat(0, 1000)] | ||
| idle = 1000 | ||
| user = 0 | ||
| for _ in range(40): | ||
| user += 10000 | ||
| samples.append(_stat(user, idle)) | ||
| return samples | ||
|
|
||
|
|
||
| def test_actuation_enforces_vcpu_cap(controller, fake_backend, run_threshold, fast_threshold): | ||
| """A managed VM's pool stays within its vCPU count.""" | ||
|
Collaborator
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. I generally prefer documenting a test more explicitly, e.g. "Test that |
||
| fake_backend.prepare_healthy_vm(threads=3, util=0.95, vcpu=4) | ||
| run_threshold(fast_threshold) | ||
|
|
||
| wait_for( | ||
| lambda: fake_backend.calls() == [4], | ||
| timeout=10, | ||
| description="scale up to the vCPU count", | ||
| ) | ||
| assert fake_backend.calls() == [4], controller.logs() | ||
| assert fake_backend.thread_count() == 4 | ||
|
|
||
| # Keep util at 0.95 and ensure it doesn't scale to 5. | ||
| # | ||
| # TODO make a fake engine which this test can use to see if the controller | ||
| # really does enforce this. Currently hard to distinguish between engine and | ||
| # controller ensuring not 5. | ||
| time.sleep(POLL_S * 5) | ||
| assert 5 not in fake_backend.calls() | ||
|
|
||
|
|
||
| def test_actuation_enforces_thread_maximum( | ||
| controller, fake_backend, run_threshold, fast_threshold | ||
| ): | ||
| """Controller maximum prevents further scale-up.""" | ||
| fake_backend.prepare_healthy_vm(threads=2, util=0.95, vcpu=8) | ||
| run_threshold( | ||
| fast_threshold, | ||
| min_thread_count=2, | ||
| max_thread_count=3, | ||
| cooldown_secs=0, | ||
| ) | ||
|
|
||
| wait_for( | ||
| lambda: fake_backend.calls() == [3], | ||
| timeout=10, | ||
| description="scale up to the controller maximum", | ||
| ) | ||
| assert fake_backend.thread_count() == 3 | ||
|
|
||
| fake_backend.clear_calls() | ||
| time.sleep(POLL_S * 5) | ||
| assert fake_backend.calls() == [], controller.logs() | ||
| assert fake_backend.thread_count() == 3 | ||
|
|
||
|
|
||
| def test_actuation_enforces_cooldown( | ||
| controller, fake_backend, run_threshold, fast_threshold | ||
| ): | ||
| """Cooldown prevents scale-down immediately after a scale-up.""" | ||
| fake_backend.prepare_healthy_vm(threads=2, util=0.95, vcpu=8) | ||
| run_threshold( | ||
| fast_threshold, | ||
| min_thread_count=2, | ||
| max_thread_count=3, | ||
| cooldown_secs=30, | ||
| ) | ||
| wait_for( | ||
| lambda: len(fake_backend.calls()) > 0, | ||
| timeout=10, | ||
| description="scale action", | ||
| ) | ||
|
|
||
| # Drop below the threshold and ensure there are no scale actions during | ||
| # the cooldown. | ||
| fake_backend.clear_calls() | ||
| fake_backend.set_util(0.0) | ||
| time.sleep(POLL_S * 5) | ||
| assert fake_backend.calls() == [], controller.logs() | ||
| assert fake_backend.thread_count() == 3 | ||
|
|
||
|
|
||
| def test_actuation_enforces_thread_minimum( | ||
| controller, fake_backend, run_threshold, fast_threshold | ||
| ): | ||
| """Controller minimum prevents scaling actions.""" | ||
| fake_backend.prepare_healthy_vm(threads=4, util=0.00, vcpu=8) | ||
| run_threshold( | ||
| fast_threshold, | ||
| min_thread_count=2, | ||
| max_thread_count=3, | ||
| cooldown_secs=0, | ||
| ) | ||
| wait_for( | ||
| lambda: fake_backend.calls() == [3, 2], | ||
| timeout=10, | ||
| description="scale down to the controller minimum", | ||
| ) | ||
| assert fake_backend.thread_count() == 2 | ||
|
|
||
| # Stay below the threshold and ensure there isn't another scale action | ||
| fake_backend.clear_calls() | ||
| time.sleep(POLL_S * 5) | ||
| assert fake_backend.calls() == [], controller.logs() | ||
| assert fake_backend.thread_count() == 2 | ||
|
|
||
|
|
||
| def test_actuation_enforces_host_cpu_scale_up_ceiling( | ||
| controller, fake_backend, mock_proc, run_threshold, fast_threshold | ||
| ): | ||
| """High host CPU prevents another threshold-engine scale-up.""" | ||
| mock_proc.register_sequence("/stat", _high_host_sequence()) | ||
| fake_backend.prepare_healthy_vm(threads=1, util=0.95, vcpu=8) | ||
| run_threshold( | ||
| fast_threshold, | ||
| host_cpu_scale_up_ceiling_percent=50, | ||
| cooldown_secs=0 | ||
| ) | ||
| wait_for( | ||
| lambda: fake_backend.calls() == [2], | ||
| timeout=10, | ||
| description="first scale-up before the host ceiling is known", | ||
| ) | ||
| fake_backend.clear_calls() | ||
| time.sleep(POLL_S * 5) | ||
| assert fake_backend.calls() == [], controller.logs() | ||
| assert fake_backend.thread_count() == 2 | ||
|
|
||
|
|
||
| def test_tick_refreshes_evaluates_and_drops_failed_instances( | ||
| controller, fake_backend, run_threshold, fast_threshold | ||
| ): | ||
| """One poll refreshes a healthy VM and drops a VM whose snapshot fails.""" | ||
| fake_backend.prepare_healthy_vm(threads=4, util=0.95, vcpu=1) | ||
| fake_backend.add_vm( | ||
| FAILED_VM, | ||
| thread_count=2, | ||
| vcpu_count=4, | ||
| per_thread_util=0.95, | ||
| fail_snapshot=True, | ||
| ) | ||
| run_threshold(fast_threshold) | ||
|
|
||
| wait_for( | ||
| lambda: controller.tracked() == 1 | ||
| and controller.thread_count(HEALTHY_VM) == fake_backend.thread_count(), | ||
| timeout=10, | ||
| description="healthy VM status after the failed VM is dropped", | ||
| ) | ||
|
|
||
| stats = controller.stats() | ||
| assert stats is not None | ||
| vms = {vm["vm"]: vm for vm in stats["vms"]} | ||
| assert vms[HEALTHY_VM]["alive"] is True | ||
| assert vms[HEALTHY_VM]["thread_count"] == fake_backend.thread_count() | ||
| assert vms[FAILED_VM]["alive"] is False | ||
| assert fake_backend.calls(FAILED_VM) == [], controller.logs() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the tests should come in a separate commit.