Add concurrent multi-VM CLI actions
Teach the lifecycle and set commands to accept multiple VM refs, resolve them from one vm list snapshot, dedupe repeated refs, and fan out the existing single-target RPCs concurrently. Valid targets still run when other refs are ambiguous or missing, and batch output stays in first-seen order. Refactor the daemon off the single global VM mutation lock by adding per-VM locks for start/stop/restart/delete/kill/set, touch, reconcile, stale-stop, and stats updates. That keeps same-VM operations serialized while allowing different VMs to progress in parallel, including newly created VMs once their ID exists. Verified with go test ./... and make build.
This commit is contained in:
parent
2d5bcb5516
commit
4812693c1e
5 changed files with 542 additions and 118 deletions
|
|
@ -2,11 +2,13 @@ package cli
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/model"
|
||||
|
|
@ -155,6 +157,97 @@ func TestVMSetParamsFromFlagsRejectsNonPositiveCPUAndMemory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveVMTargetsDeduplicatesAndReportsErrors(t *testing.T) {
|
||||
vms := []model.VMRecord{
|
||||
testCLIResolvedVM("alpha-id", "alpha"),
|
||||
testCLIResolvedVM("alpine-id", "alpine"),
|
||||
testCLIResolvedVM("bravo-id", "bravo"),
|
||||
}
|
||||
|
||||
targets, errs := resolveVMTargets(vms, []string{"alpha", "alpha-id", "al", "missing", "br"})
|
||||
|
||||
if len(targets) != 2 {
|
||||
t.Fatalf("len(targets) = %d, want 2", len(targets))
|
||||
}
|
||||
if targets[0].VM.ID != "alpha-id" || targets[0].Ref != "alpha" {
|
||||
t.Fatalf("targets[0] = %+v, want alpha target", targets[0])
|
||||
}
|
||||
if targets[1].VM.ID != "bravo-id" || targets[1].Ref != "br" {
|
||||
t.Fatalf("targets[1] = %+v, want bravo target", targets[1])
|
||||
}
|
||||
if len(errs) != 2 {
|
||||
t.Fatalf("len(errs) = %d, want 2", len(errs))
|
||||
}
|
||||
if errs[0].Ref != "al" || !strings.Contains(errs[0].Err.Error(), "multiple VMs match") {
|
||||
t.Fatalf("errs[0] = %+v, want ambiguous prefix", errs[0])
|
||||
}
|
||||
if errs[1].Ref != "missing" || !strings.Contains(errs[1].Err.Error(), `vm "missing" not found`) {
|
||||
t.Fatalf("errs[1] = %+v, want missing vm", errs[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveVMRefPrefersExactMatchBeforePrefix(t *testing.T) {
|
||||
vms := []model.VMRecord{
|
||||
testCLIResolvedVM("1111111111111111111111111111111111111111111111111111111111111111", "alpha"),
|
||||
testCLIResolvedVM("alpha222222222222222222222222222222222222222222222222222222222222", "bravo"),
|
||||
}
|
||||
|
||||
vm, err := resolveVMRef(vms, "alpha")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveVMRef(alpha): %v", err)
|
||||
}
|
||||
if vm.Name != "alpha" {
|
||||
t.Fatalf("resolveVMRef(alpha) = %+v, want exact-name vm", vm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteVMActionBatchRunsConcurrentlyAndPreservesOrder(t *testing.T) {
|
||||
targets := []resolvedVMTarget{
|
||||
{Ref: "alpha", VM: testCLIResolvedVM("alpha-id", "alpha")},
|
||||
{Ref: "bravo", VM: testCLIResolvedVM("bravo-id", "bravo")},
|
||||
}
|
||||
|
||||
started := make(chan string, len(targets))
|
||||
release := make(chan struct{})
|
||||
done := make(chan []vmBatchActionResult, 1)
|
||||
go func() {
|
||||
done <- executeVMActionBatch(context.Background(), targets, func(ctx context.Context, id string) (model.VMRecord, error) {
|
||||
started <- id
|
||||
<-release
|
||||
return model.VMRecord{ID: id, Name: id}, nil
|
||||
})
|
||||
}()
|
||||
|
||||
for range targets {
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("batch actions did not overlap")
|
||||
}
|
||||
}
|
||||
|
||||
close(release)
|
||||
|
||||
var results []vmBatchActionResult
|
||||
select {
|
||||
case results = <-done:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("executeVMActionBatch did not finish")
|
||||
}
|
||||
|
||||
if len(results) != len(targets) {
|
||||
t.Fatalf("len(results) = %d, want %d", len(results), len(targets))
|
||||
}
|
||||
for index, result := range results {
|
||||
if result.Target.Ref != targets[index].Ref {
|
||||
t.Fatalf("results[%d].Target.Ref = %q, want %q", index, result.Target.Ref, targets[index].Ref)
|
||||
}
|
||||
if result.VM.ID != targets[index].VM.ID {
|
||||
t.Fatalf("results[%d].VM.ID = %q, want %q", index, result.VM.ID, targets[index].VM.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandArgs(t *testing.T) {
|
||||
args, err := sshCommandArgs(model.DaemonConfig{SSHKeyPath: "/bundle/id_ed25519"}, "172.16.0.2", []string{"--", "uname", "-a"})
|
||||
if err != nil {
|
||||
|
|
@ -312,3 +405,7 @@ func TestAbsolutizeImageBuildPaths(t *testing.T) {
|
|||
t.Fatalf("params = %+v, want %+v", params, want)
|
||||
}
|
||||
}
|
||||
|
||||
func testCLIResolvedVM(id, name string) model.VMRecord {
|
||||
return model.VMRecord{ID: id, Name: name}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue