From 0561e9aa17bac775e651690d89fcbf04c61709e0 Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Sat, 3 Oct 2026 13:23:09 -0700 Subject: [PATCH] glutton: add RunScript, a script tree run inside the actor as one request A benchmark that wants an actor to do a whole on-sandbox activity, a build, a test run, a file churn, had to issue every resource operation as its own request through the router. That costs a round trip per operation, and it leaves the driver unable to hold the actor busy for a chosen length of time without knowing how long each operation takes on the sandbox at hand. RunScript takes the activity as a script and runs it in-process, holding the request open until it finishes. A script is a tree. A Block runs its steps in order and may cycle them for a wall-clock budget; a Step is a nested Block or an Operation; an Operation runs its requests at the same time and waits for all of them. A budget is checked between passes: the pass under way finishes, so a loop overruns by at most one pass and no request is cut short, and a nested block stops cycling once any ancestor's budget has elapsed the same way. The only errors are the caller's context ending and a failing request, which names the request's path in the tree and keeps its status code. The request set is the service's work requests plus sleep. UseCPU and OpenFD are left out because they install a background load that outlives the script; BurnCPU is the CPU spike. The response mirrors the tree with summed vital stats at every node: requests run, bytes written and read to disk and RAM, burn iterations, time slept, and the node's own wall clock and entry count. A node inside a looped block sums over its passes, so the response is the size of the script however long it ran, and the root carries the totals. The fake actor answers a script with the mirrored tree, so drivers can be tested against it. --- internal/benchmarking/glutton/fake/server.go | 53 ++ internal/benchmarking/glutton/glutton.go | 1 + internal/benchmarking/glutton/script.go | 312 +++++++ internal/benchmarking/glutton/script_test.go | 304 +++++++ internal/benchmarking/glutton/server.go | 1 + internal/proto/glutton/glutton.pb.go | 811 ++++++++++++++++++- internal/proto/glutton/glutton.proto | 112 +++ internal/proto/glutton/glutton_grpc.pb.go | 54 ++ 8 files changed, 1618 insertions(+), 30 deletions(-) create mode 100644 internal/benchmarking/glutton/script.go create mode 100644 internal/benchmarking/glutton/script_test.go diff --git a/internal/benchmarking/glutton/fake/server.go b/internal/benchmarking/glutton/fake/server.go index 15bc983f93..1a6821bcae 100644 --- a/internal/benchmarking/glutton/fake/server.go +++ b/internal/benchmarking/glutton/fake/server.go @@ -41,6 +41,7 @@ const ( IngestRoute = glutton.IngestRoute PingRoute = glutton.PingRoute UseCPURoute = glutton.UseCPURoute + RunScriptRoute = glutton.RunScriptRoute ) // Server is an httptest-backed stand-in for a glutton actor holding one file. @@ -72,6 +73,7 @@ type Server struct { burnMillis []int64 ingestSizes []int64 cpuRequests []*gluttonpb.UseCPURequest + scripts []*gluttonpb.RunScriptRequest } func (s *Server) reportedDigest() []byte { @@ -135,6 +137,13 @@ func (s *Server) RecordedRAMReadSizes() []string { return append([]string(nil), s.ramReadSizes...) } +// RecordedScripts returns each /runscript request. +func (s *Server) RecordedScripts() []*gluttonpb.RunScriptRequest { + s.mu.Lock() + defer s.mu.Unlock() + return append([]*gluttonpb.RunScriptRequest(nil), s.scripts...) +} + // RecordedCPURequests returns each /usecpu request. func (s *Server) RecordedCPURequests() []*gluttonpb.UseCPURequest { s.mu.Lock() @@ -317,6 +326,26 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) { resp, _ := proto.Marshal(&gluttonpb.UseCPUResponse{NumCores: req.GetNumCores()}) _, _ = w.Write(resp) + case RunScriptRoute: + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var req gluttonpb.RunScriptRequest + if err := proto.Unmarshal(body, &req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.mu.Lock() + s.scripts = append(s.scripts, &req) + s.mu.Unlock() + + // The fake does no work, so the script is instant: every node + // entered once, every request run. + resp, _ := proto.Marshal(&gluttonpb.RunScriptResponse{Result: mirrorBlock(req.GetScript())}) + _, _ = w.Write(resp) + default: http.NotFound(w, r) } @@ -335,3 +364,27 @@ func (s *Server) RecordedIngestSizes() []int64 { defer s.mu.Unlock() return append([]int64(nil), s.ingestSizes...) } + +// mirrorBlock builds the result tree the real glutton would return for a +// script that cost nothing: one pass per node and one request per leaf. +func mirrorBlock(block *gluttonpb.Block) *gluttonpb.StepResult { + result := &gluttonpb.StepResult{Stats: &gluttonpb.Stats{Passes: 1}} + for _, step := range block.GetSteps() { + var child *gluttonpb.StepResult + switch kind := step.GetKind().(type) { + case *gluttonpb.Step_Block: + child = mirrorBlock(kind.Block) + case *gluttonpb.Step_Operation: + child = &gluttonpb.StepResult{Stats: &gluttonpb.Stats{Passes: 1}} + for range kind.Operation.GetRequests() { + child.Children = append(child.Children, &gluttonpb.StepResult{Stats: &gluttonpb.Stats{Passes: 1, RequestsRun: 1}}) + child.Stats.RequestsRun++ + } + default: + child = &gluttonpb.StepResult{Stats: &gluttonpb.Stats{}} + } + result.Children = append(result.Children, child) + result.Stats.RequestsRun += child.GetStats().GetRequestsRun() + } + return result +} diff --git a/internal/benchmarking/glutton/glutton.go b/internal/benchmarking/glutton/glutton.go index 17e438bfbd..2ea38bde84 100644 --- a/internal/benchmarking/glutton/glutton.go +++ b/internal/benchmarking/glutton/glutton.go @@ -44,4 +44,5 @@ const ( BurnCPURoute = "/burncpu" IngestRoute = "/ingest" UseCPURoute = "/usecpu" + RunScriptRoute = "/runscript" ) diff --git a/internal/benchmarking/glutton/script.go b/internal/benchmarking/glutton/script.go new file mode 100644 index 0000000000..7fb95b5223 --- /dev/null +++ b/internal/benchmarking/glutton/script.go @@ -0,0 +1,312 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package glutton + +import ( + "context" + "fmt" + "time" + + "golang.org/x/sync/errgroup" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" +) + +// RunScript runs the request's script tree in-process and holds the +// request open until it finishes. The tree is checked for shape before +// anything runs; a request's own argument errors surface when it runs, +// with its path in the tree and kind prepended and its status code kept, +// so a caller can tell a bad script from a failed request. See the proto +// for what a block, an operation, and the stats mean. +func (s *Service) RunScript(ctx context.Context, req *gluttonpb.RunScriptRequest) (*gluttonpb.RunScriptResponse, error) { + if req.GetScript() == nil { + return nil, status.Error(codes.InvalidArgument, "script is required") + } + if err := validateBlock(req.GetScript(), "script"); err != nil { + return nil, err + } + result, err := s.runBlock(ctx, req.GetScript(), "script", time.Time{}) + if err != nil { + return nil, err + } + return &gluttonpb.RunScriptResponse{Result: result}, nil +} + +// validateBlock rejects what can be seen without running anything: an +// empty block or operation, a step or request with nothing set, a negative +// budget or sleep. path names the node in messages. +func validateBlock(block *gluttonpb.Block, path string) error { + if len(block.GetSteps()) == 0 { + return status.Errorf(codes.InvalidArgument, "%s: steps is required", path) + } + if block.GetLoopDurationMs() < 0 { + return status.Errorf(codes.InvalidArgument, "%s: loop_duration_ms must be non-negative", path) + } + for i, step := range block.GetSteps() { + stepPath := fmt.Sprintf("%s.steps[%d]", path, i) + switch kind := step.GetKind().(type) { + case *gluttonpb.Step_Block: + if err := validateBlock(kind.Block, stepPath+".block"); err != nil { + return err + } + case *gluttonpb.Step_Operation: + if err := validateOperation(kind.Operation, stepPath+".operation"); err != nil { + return err + } + default: + return status.Errorf(codes.InvalidArgument, "%s: no kind set", stepPath) + } + } + return nil +} + +func validateOperation(op *gluttonpb.Operation, path string) error { + if len(op.GetRequests()) == 0 { + return status.Errorf(codes.InvalidArgument, "%s: requests is required", path) + } + for i, request := range op.GetRequests() { + requestPath := fmt.Sprintf("%s.requests[%d]", path, i) + switch kind := request.GetKind().(type) { + case nil: + return status.Errorf(codes.InvalidArgument, "%s: no kind set", requestPath) + case *gluttonpb.Request_Sleep: + if kind.Sleep.GetDurationMs() < 0 { + return status.Errorf(codes.InvalidArgument, "%s (sleep): duration_ms must be non-negative", requestPath) + } + } + } + return nil +} + +// runBlock runs the block's steps in order and, when the block loops, +// again and again until a budget has elapsed. deadline is the earliest +// budget among the block's ancestors, zero for none; the block's own +// budget tightens it for the block and everything beneath. A budget is +// checked between passes only: the pass in progress finishes, so a loop +// overruns its budget by at most one pass and nothing is cut mid-flight. +// The only errors are the caller's context ending and a failing request. +func (s *Service) runBlock(ctx context.Context, block *gluttonpb.Block, path string, deadline time.Time) (*gluttonpb.StepResult, error) { + start := time.Now() + result := newResult(len(block.GetSteps())) + if budget := block.GetLoopDurationMs(); budget > 0 { + own := start.Add(time.Duration(budget) * time.Millisecond) + if deadline.IsZero() || own.Before(deadline) { + deadline = own + } + } + for { + result.Stats.Passes++ + for i, step := range block.GetSteps() { + child, err := s.runStep(ctx, step, fmt.Sprintf("%s.steps[%d]", path, i), deadline) + if child != nil { + addResult(result.Children[i], child) + addTotals(result.Stats, child.Stats) + } + if err != nil { + result.Stats.ElapsedMs = time.Since(start).Milliseconds() + return result, err + } + } + if block.GetLoopDurationMs() == 0 || !time.Now().Before(deadline) { + break + } + } + result.Stats.ElapsedMs = time.Since(start).Milliseconds() + return result, nil +} + +func (s *Service) runStep(ctx context.Context, step *gluttonpb.Step, path string, deadline time.Time) (*gluttonpb.StepResult, error) { + switch kind := step.GetKind().(type) { + case *gluttonpb.Step_Block: + return s.runBlock(ctx, kind.Block, path+".block", deadline) + case *gluttonpb.Step_Operation: + return s.runOperation(ctx, kind.Operation, path+".operation") + default: + return nil, status.Errorf(codes.InvalidArgument, "%s: no kind set", path) + } +} + +// runOperation runs the operation's requests at the same time. The first +// failure cancels the rest and is the error returned; the result still +// carries the stats of every request that ran, failed ones included. +func (s *Service) runOperation(ctx context.Context, op *gluttonpb.Operation, path string) (*gluttonpb.StepResult, error) { + start := time.Now() + result := newResult(len(op.GetRequests())) + group, groupCtx := errgroup.WithContext(ctx) + for i, request := range op.GetRequests() { + group.Go(func() error { + stats, err := s.runRequest(groupCtx, request) + if stats != nil { + result.Children[i].Stats = stats + } + if err != nil { + return prefixError(fmt.Sprintf("%s.requests[%d] (%s)", path, i, requestKind(request)), err) + } + return nil + }) + } + err := group.Wait() + for _, child := range result.Children { + addTotals(result.Stats, child.Stats) + } + result.Stats.Passes = 1 + result.Stats.ElapsedMs = time.Since(start).Milliseconds() + return result, err +} + +// runRequest dispatches one request to the service method it names and +// reports what it did. Results other than the stats are dropped. +func (s *Service) runRequest(ctx context.Context, request *gluttonpb.Request) (*gluttonpb.Stats, error) { + start := time.Now() + stats := &gluttonpb.Stats{Passes: 1, RequestsRun: 1} + var err error + switch kind := request.GetKind().(type) { + case *gluttonpb.Request_WriteRam: + _, err = s.WriteRAM(ctx, kind.WriteRam) + if err == nil { + stats.RamBytesWritten, _ = parseBytes(kind.WriteRam.GetSize()) + } + case *gluttonpb.Request_ReadRam: + var resp *gluttonpb.ReadRAMResponse + resp, err = s.ReadRAM(ctx, kind.ReadRam) + stats.RamBytesRead = resp.GetSize() + case *gluttonpb.Request_WriteDisk: + _, err = s.WriteDisk(ctx, kind.WriteDisk) + if err == nil { + stats.DiskBytesWritten = int64(kind.WriteDisk.GetSize()) + } + case *gluttonpb.Request_ReadDisk: + var resp *gluttonpb.ReadDiskResponse + resp, err = s.ReadDisk(ctx, kind.ReadDisk) + stats.DiskBytesRead = resp.GetSize() + case *gluttonpb.Request_BurnCpu: + var resp *gluttonpb.BurnCPUResponse + resp, err = s.BurnCPU(ctx, kind.BurnCpu) + stats.BurnIterations = resp.GetIterations() + case *gluttonpb.Request_Ingest: + _, err = s.Ingest(ctx, kind.Ingest) + if err == nil { + stats.DiskBytesWritten = int64(len(kind.Ingest.GetPayload())) + } + case *gluttonpb.Request_Sleep: + err = sleep(ctx, time.Duration(kind.Sleep.GetDurationMs())*time.Millisecond) + stats.SleptMs = time.Since(start).Milliseconds() + default: + err = status.Errorf(codes.InvalidArgument, "unknown request %T", kind) + } + stats.ElapsedMs = time.Since(start).Milliseconds() + return stats, err +} + +// sleep idles for duration or until ctx ends, whichever comes first. +func sleep(ctx context.Context, duration time.Duration) error { + if duration <= 0 { + return nil + } + timer := time.NewTimer(duration) + defer timer.Stop() + select { + case <-timer.C: + return nil + case <-ctx.Done(): + return status.FromContextError(ctx.Err()).Err() + } +} + +// newResult is a node with zeroed stats and n zeroed children, ready to +// have each pass added in. +func newResult(n int) *gluttonpb.StepResult { + result := &gluttonpb.StepResult{Stats: &gluttonpb.Stats{}, Children: make([]*gluttonpb.StepResult, n)} + for i := range result.Children { + result.Children[i] = &gluttonpb.StepResult{Stats: &gluttonpb.Stats{}} + } + return result +} + +// addResult folds one entry's result tree into the accumulated node of the +// same shape. A child the entry did not produce (an operation's request +// that never started) adds nothing. +func addResult(dst, src *gluttonpb.StepResult) { + addStats(dst.Stats, src.Stats) + for len(dst.Children) < len(src.Children) { + dst.Children = append(dst.Children, &gluttonpb.StepResult{Stats: &gluttonpb.Stats{}}) + } + for i, child := range src.Children { + if child != nil { + addResult(dst.Children[i], child) + } + } +} + +// addStats sums src into dst, field by field: one node's later entry +// added to its earlier ones. +func addStats(dst, src *gluttonpb.Stats) { + if src == nil { + return + } + dst.ElapsedMs += src.ElapsedMs + dst.Passes += src.Passes + addTotals(dst, src) +} + +// addTotals sums a child's work into its parent: every field but passes, +// which count the parent's own entries, and elapsed, which the parent +// measures itself. +func addTotals(dst, src *gluttonpb.Stats) { + if src == nil { + return + } + dst.RequestsRun += src.RequestsRun + dst.DiskBytesWritten += src.DiskBytesWritten + dst.DiskBytesRead += src.DiskBytesRead + dst.RamBytesWritten += src.RamBytesWritten + dst.RamBytesRead += src.RamBytesRead + dst.BurnIterations += src.BurnIterations + dst.SleptMs += src.SleptMs +} + +// requestKind is the proto field name of the request that is set. +func requestKind(request *gluttonpb.Request) string { + switch request.GetKind().(type) { + case *gluttonpb.Request_WriteRam: + return "write_ram" + case *gluttonpb.Request_ReadRam: + return "read_ram" + case *gluttonpb.Request_WriteDisk: + return "write_disk" + case *gluttonpb.Request_ReadDisk: + return "read_disk" + case *gluttonpb.Request_BurnCpu: + return "burn_cpu" + case *gluttonpb.Request_Ingest: + return "ingest" + case *gluttonpb.Request_Sleep: + return "sleep" + } + return "unset" +} + +// prefixError puts the failing request's path ahead of err's message, +// keeping its status code so the HTTP mapping and the caller's +// classification still apply. +func prefixError(where string, err error) error { + st, ok := status.FromError(err) + if !ok { + return status.Errorf(codes.Internal, "%s: %v", where, err) + } + return status.Error(st.Code(), fmt.Sprintf("%s: %s", where, st.Message())) +} diff --git a/internal/benchmarking/glutton/script_test.go b/internal/benchmarking/glutton/script_test.go new file mode 100644 index 0000000000..82459d5259 --- /dev/null +++ b/internal/benchmarking/glutton/script_test.go @@ -0,0 +1,304 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package glutton + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + + gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" +) + +// Script builders, so the tests read like the trees they send. + +func writeDisk(key string, size int32) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_WriteDisk{WriteDisk: &gluttonpb.WriteDiskRequest{Key: key, Size: size}}} +} + +func readDisk(key string) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_ReadDisk{ReadDisk: &gluttonpb.ReadDiskRequest{Key: key, ReadMode: gluttonpb.ReadMode_READ_MODE_DIGEST_ONLY}}} +} + +func writeRAM(key, size string) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_WriteRam{WriteRam: &gluttonpb.WriteRAMRequest{Key: key, Size: size}}} +} + +func readRAM(key string) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_ReadRam{ReadRam: &gluttonpb.ReadRAMRequest{Key: key}}} +} + +func burn(ms int64) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_BurnCpu{BurnCpu: &gluttonpb.BurnCPURequest{DurationMs: ms}}} +} + +func sleepFor(ms int64) *gluttonpb.Request { + return &gluttonpb.Request{Kind: &gluttonpb.Request_Sleep{Sleep: &gluttonpb.SleepRequest{DurationMs: ms}}} +} + +// op is a step holding one operation of the given requests, run together. +func op(requests ...*gluttonpb.Request) *gluttonpb.Step { + return &gluttonpb.Step{Kind: &gluttonpb.Step_Operation{Operation: &gluttonpb.Operation{Requests: requests}}} +} + +// seq is a step holding a block of the given steps, run in order once. +func seq(steps ...*gluttonpb.Step) *gluttonpb.Step { + return &gluttonpb.Step{Kind: &gluttonpb.Step_Block{Block: &gluttonpb.Block{Steps: steps}}} +} + +// loop is a step holding a block that cycles its steps for budgetMs. +func loop(budgetMs int64, steps ...*gluttonpb.Step) *gluttonpb.Step { + return &gluttonpb.Step{Kind: &gluttonpb.Step_Block{Block: &gluttonpb.Block{Steps: steps, LoopDurationMs: budgetMs}}} +} + +func script(steps ...*gluttonpb.Step) *gluttonpb.RunScriptRequest { + return &gluttonpb.RunScriptRequest{Script: &gluttonpb.Block{Steps: steps}} +} + +func run(t *testing.T, svc *Service, req *gluttonpb.RunScriptRequest) *gluttonpb.StepResult { + t.Helper() + resp, err := svc.RunScript(context.Background(), req) + if err != nil { + t.Fatalf("RunScript: %v", err) + } + return resp.GetResult() +} + +// Steps run in order, each request has its usual effect, and the result +// mirrors the tree with the totals summed to the root. +func TestRunScriptRunsStepsInOrderAndSumsStats(t *testing.T) { + svc := newTestService(t) + result := run(t, svc, script( + op(writeDisk("scripted", 1024)), + op(readDisk("scripted")), + seq(op(writeRAM("arr", "8Ki")), op(readRAM("arr"))), + op(burn(1), sleepFor(1)), + )) + root := result.GetStats() + if root.GetPasses() != 1 || root.GetRequestsRun() != 6 { + t.Errorf("root passes=%d requests_run=%d, want 1/6", root.GetPasses(), root.GetRequestsRun()) + } + if root.GetDiskBytesWritten() != 1024 || root.GetDiskBytesRead() != 1024 { + t.Errorf("disk bytes written=%d read=%d, want 1024/1024", root.GetDiskBytesWritten(), root.GetDiskBytesRead()) + } + if root.GetRamBytesWritten() != 8192 || root.GetRamBytesRead() != 8192 { + t.Errorf("ram bytes written=%d read=%d, want 8192/8192", root.GetRamBytesWritten(), root.GetRamBytesRead()) + } + if root.GetBurnIterations() < 1 || root.GetSleptMs() < 1 { + t.Errorf("burn_iterations=%d slept_ms=%d, want both positive", root.GetBurnIterations(), root.GetSleptMs()) + } + if len(result.GetChildren()) != 4 { + t.Fatalf("root has %d children, want 4 steps", len(result.GetChildren())) + } + nested := result.GetChildren()[2] + if len(nested.GetChildren()) != 2 || nested.GetStats().GetRequestsRun() != 2 || nested.GetStats().GetRamBytesWritten() != 8192 { + t.Errorf("nested block = %v, want two children and the RAM totals", nested) + } + // An operation's children are its requests, each with its own stats. + parallel := result.GetChildren()[3] + if len(parallel.GetChildren()) != 2 || parallel.GetChildren()[0].GetStats().GetBurnIterations() < 1 || parallel.GetChildren()[1].GetStats().GetSleptMs() < 1 { + t.Errorf("operation = %v, want a burn child and a sleep child", parallel) + } +} + +// Requests in one operation overlap: two sleeps of 100ms take about 100ms, +// and the operation's elapsed is its longest request, not their sum. +func TestRunScriptOperationRunsRequestsTogether(t *testing.T) { + svc := newTestService(t) + start := time.Now() + result := run(t, svc, script(op(sleepFor(100), sleepFor(100), sleepFor(100)))) + if elapsed := time.Since(start); elapsed > 250*time.Millisecond { + t.Errorf("three parallel 100ms sleeps took %v", elapsed) + } + stats := result.GetChildren()[0].GetStats() + if stats.GetSleptMs() < 300 || stats.GetElapsedMs() > 250 { + t.Errorf("operation slept_ms=%d elapsed_ms=%d, want ~300 summed and ~100 wall", stats.GetSleptMs(), stats.GetElapsedMs()) + } +} + +// A looped block cycles until its budget and finishes the pass it is in: +// every pass is whole, the loop overruns by at most one pass, and the +// stats count every pass. +func TestRunScriptLoopsUntilTheBudget(t *testing.T) { + svc := newTestService(t) + start := time.Now() + result := run(t, svc, script(loop(100, op(sleepFor(30)), op(writeDisk("churn", 16))))) + elapsed := time.Since(start) + if elapsed < 100*time.Millisecond || elapsed > 300*time.Millisecond { + t.Errorf("a 100ms loop returned after %v", elapsed) + } + looped := result.GetChildren()[0].GetStats() + if looped.GetPasses() < 4 || looped.GetPasses() > 5 { + t.Errorf("passes = %d, want 4 whole 30ms passes to cover 100ms", looped.GetPasses()) + } + // Whole passes: as many writes as sleeps, and the sleeps complete. + if looped.GetDiskBytesWritten() != 16*looped.GetPasses() || looped.GetSleptMs() < 30*(looped.GetPasses()-1) { + t.Errorf("loop stats = %v, want %d whole passes", looped, looped.GetPasses()) + } + if root := result.GetStats(); root.GetPasses() != 1 || root.GetDiskBytesWritten() != looped.GetDiskBytesWritten() { + t.Errorf("root = %v, want one pass and the loop's totals", root) + } +} + +// An inner loop stops cycling once the outer one's budget has elapsed, +// finishing its pass; that is not an error. +func TestRunScriptNestedLoopsShareTheBudget(t *testing.T) { + svc := newTestService(t) + start := time.Now() + result := run(t, svc, script(loop(100, loop(5000, op(sleepFor(20)))))) + if elapsed := time.Since(start); elapsed > 300*time.Millisecond { + t.Errorf("a 5s loop inside a 100ms loop ran for %v", elapsed) + } + inner := result.GetChildren()[0].GetChildren()[0].GetStats() + if inner.GetPasses() < 5 || inner.GetSleptMs() < 20*inner.GetPasses() { + t.Errorf("inner loop = %v, want ~5 whole 20ms passes", inner) + } +} + +// Shape problems are rejected before any step runs, with the node named. +func TestRunScriptRejectsShape(t *testing.T) { + svc := newTestService(t) + for _, tc := range []struct { + name string + req *gluttonpb.RunScriptRequest + msg string + }{ + {"no script", &gluttonpb.RunScriptRequest{}, "script is required"}, + {"empty block", script(), "script: steps is required"}, + {"unset step", script(&gluttonpb.Step{}), "script.steps[0]: no kind set"}, + {"empty operation", script(op()), "script.steps[0].operation: requests is required"}, + {"unset request", script(op(&gluttonpb.Request{})), "script.steps[0].operation.requests[0]: no kind set"}, + {"negative sleep", script(op(sleepFor(-1))), "requests[0] (sleep)"}, + {"negative budget", script(loop(-1, op(sleepFor(0)))), "script.steps[0].block: loop_duration_ms"}, + {"deep empty block", script(seq(seq(seq()))), "script.steps[0].block.steps[0].block.steps[0].block: steps is required"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := svc.RunScript(context.Background(), tc.req) + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("error = %v, want InvalidArgument", err) + } + if !strings.Contains(err.Error(), tc.msg) { + t.Errorf("error %q does not mention %q", err, tc.msg) + } + }) + } + if _, err := svc.ReadDisk(context.Background(), &gluttonpb.ReadDiskRequest{Key: "scripted"}); status.Code(err) != codes.NotFound { + t.Errorf("a rejected script left a file behind (ReadDisk err = %v)", err) + } +} + +// A request's own error keeps its code and names the request's path; the +// script stops there. +func TestRunScriptRequestErrorsKeepTheirCode(t *testing.T) { + svc := newTestService(t) + for _, tc := range []struct { + name string + req *gluttonpb.RunScriptRequest + code codes.Code + msg string + }{ + {"bad key", script(op(sleepFor(0)), op(writeDisk("../x", 1))), codes.InvalidArgument, "script.steps[1].operation.requests[0] (write_disk)"}, + {"missing file", script(seq(op(readDisk("nothing")))), codes.NotFound, "script.steps[0].block.steps[0].operation.requests[0] (read_disk)"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := svc.RunScript(context.Background(), tc.req) + st, ok := status.FromError(err) + if !ok || st.Code() != tc.code { + t.Fatalf("error = %v, want code %v", err, tc.code) + } + if !strings.Contains(st.Message(), tc.msg) { + t.Errorf("message %q does not mention %q", st.Message(), tc.msg) + } + }) + } + // A failing request inside a loop is an error, not the end of the loop. + _, err := svc.RunScript(context.Background(), script(loop(100, op(readDisk("nothing"))))) + if status.Code(err) != codes.NotFound { + t.Errorf("a failing request in a loop returned %v, want NotFound", err) + } + // One failing request cancels its siblings in the operation. + start := time.Now() + _, err = svc.RunScript(context.Background(), script(op(readDisk("nothing"), sleepFor(5000)))) + if status.Code(err) != codes.NotFound || time.Since(start) > time.Second { + t.Errorf("sibling failure: err %v after %v, want NotFound promptly", err, time.Since(start)) + } +} + +// A caller that goes away ends the script, mid-sleep or at the next +// request, so a disconnected request does not keep the sandbox busy. That +// is an error, unlike a budget running out. +func TestRunScriptStopsWhenTheContextEnds(t *testing.T) { + svc := newTestService(t) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + start := time.Now() + _, err := svc.RunScript(ctx, script(loop(10000, op(sleepFor(5000))))) + if status.Code(err) != codes.DeadlineExceeded { + t.Fatalf("error = %v, want DeadlineExceeded", err) + } + if elapsed := time.Since(start); elapsed > time.Second { + t.Errorf("took %v to notice the context ended", elapsed) + } +} + +// The HTTP route carries the same request and maps a request's status code +// the way every other route does. +func TestRunScriptHTTPRoute(t *testing.T) { + svc := newTestService(t) + handler, err := Handler(ModeHTTP, svc) + if err != nil { + t.Fatal(err) + } + ts := httptest.NewServer(handler) + defer ts.Close() + + post := func(req *gluttonpb.RunScriptRequest) (*http.Response, []byte) { + t.Helper() + body, _ := proto.Marshal(req) + res, err := http.Post(ts.URL+RunScriptRoute, "application/x-protobuf", bytes.NewReader(body)) + if err != nil { + t.Fatalf("POST %s: %v", RunScriptRoute, err) + } + defer res.Body.Close() + out, _ := io.ReadAll(res.Body) + return res, out + } + + res, body := post(script(op(writeDisk("viahttp", 64)), op(readDisk("viahttp")))) + if res.StatusCode != http.StatusOK { + t.Fatalf("status %d: %s", res.StatusCode, body) + } + var resp gluttonpb.RunScriptResponse + if err := proto.Unmarshal(body, &resp); err != nil { + t.Fatal(err) + } + if resp.GetResult().GetStats().GetRequestsRun() != 2 { + t.Errorf("requests_run = %d, want 2", resp.GetResult().GetStats().GetRequestsRun()) + } + + res, body = post(script(op(readDisk("absent")))) + if res.StatusCode != http.StatusNotFound || !strings.Contains(string(body), "(read_disk)") { + t.Errorf("missing file over HTTP: status %d body %q, want 404 naming the request", res.StatusCode, body) + } +} diff --git a/internal/benchmarking/glutton/server.go b/internal/benchmarking/glutton/server.go index 7b910a8573..0b773bd6d4 100644 --- a/internal/benchmarking/glutton/server.go +++ b/internal/benchmarking/glutton/server.go @@ -105,6 +105,7 @@ func newMux(svc *Service) *http.ServeMux { mux.HandleFunc(BurnCPURoute, protoRoute("BurnCPU", svc.BurnCPU)) mux.HandleFunc(IngestRoute, protoRoute("Ingest", svc.Ingest)) mux.HandleFunc(UseCPURoute, protoRoute("UseCPU", svc.UseCPU)) + mux.HandleFunc(RunScriptRoute, protoRoute("RunScript", svc.RunScript)) return mux } diff --git a/internal/proto/glutton/glutton.pb.go b/internal/proto/glutton/glutton.pb.go index 426554a768..f54608c1d5 100644 --- a/internal/proto/glutton/glutton.pb.go +++ b/internal/proto/glutton/glutton.pb.go @@ -1222,6 +1222,676 @@ func (x *UseCPUResponse) GetNumCores() int32 { return 0 } +// A Block is a list of steps run in order. With loop_duration_ms set, the +// block cycles through its steps until the budget elapses. A budget is +// checked between passes: the pass in progress finishes, so a loop +// overruns its budget by at most one pass and no request is cut short. +// A nested block stops cycling once any ancestor's budget has elapsed, +// the same way. +type Block struct { + state protoimpl.MessageState `protogen:"open.v1"` + // At least one step is required. + Steps []*Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + // Wall-clock budget to cycle through the steps for, in milliseconds; + // the pass under way when it elapses finishes. Zero runs the steps once. + LoopDurationMs int64 `protobuf:"varint,2,opt,name=loop_duration_ms,json=loopDurationMs,proto3" json:"loop_duration_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Block) Reset() { + *x = Block{} + mi := &file_glutton_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Block) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Block) ProtoMessage() {} + +func (x *Block) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Block.ProtoReflect.Descriptor instead. +func (*Block) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{21} +} + +func (x *Block) GetSteps() []*Step { + if x != nil { + return x.Steps + } + return nil +} + +func (x *Block) GetLoopDurationMs() int64 { + if x != nil { + return x.LoopDurationMs + } + return 0 +} + +// A Step is either a nested Block or an Operation. +type Step struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Kind: + // + // *Step_Block + // *Step_Operation + Kind isStep_Kind `protobuf_oneof:"kind"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Step) Reset() { + *x = Step{} + mi := &file_glutton_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Step) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Step) ProtoMessage() {} + +func (x *Step) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Step.ProtoReflect.Descriptor instead. +func (*Step) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{22} +} + +func (x *Step) GetKind() isStep_Kind { + if x != nil { + return x.Kind + } + return nil +} + +func (x *Step) GetBlock() *Block { + if x != nil { + if x, ok := x.Kind.(*Step_Block); ok { + return x.Block + } + } + return nil +} + +func (x *Step) GetOperation() *Operation { + if x != nil { + if x, ok := x.Kind.(*Step_Operation); ok { + return x.Operation + } + } + return nil +} + +type isStep_Kind interface { + isStep_Kind() +} + +type Step_Block struct { + Block *Block `protobuf:"bytes,1,opt,name=block,proto3,oneof"` +} + +type Step_Operation struct { + Operation *Operation `protobuf:"bytes,2,opt,name=operation,proto3,oneof"` +} + +func (*Step_Block) isStep_Kind() {} + +func (*Step_Operation) isStep_Kind() {} + +// An Operation runs its requests at the same time and waits for all of +// them. The first request to fail cancels the rest. RAM requests take +// turns on the glutton's single RAM lock, so only CPU, disk, and sleep +// requests overlap in practice. +type Operation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // At least one request is required. + Requests []*Request `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Operation) Reset() { + *x = Operation{} + mi := &file_glutton_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Operation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Operation) ProtoMessage() {} + +func (x *Operation) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Operation.ProtoReflect.Descriptor instead. +func (*Operation) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{23} +} + +func (x *Operation) GetRequests() []*Request { + if x != nil { + return x.Requests + } + return nil +} + +// A Request is one of the service's work requests. UseCPU and OpenFD are +// left out because they set a background load that outlives the script +// (BurnCPU is the CPU spike), and Gossip because it starts a loop. +type Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Kind: + // + // *Request_WriteRam + // *Request_ReadRam + // *Request_WriteDisk + // *Request_ReadDisk + // *Request_BurnCpu + // *Request_Ingest + // *Request_Sleep + Kind isRequest_Kind `protobuf_oneof:"kind"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Request) Reset() { + *x = Request{} + mi := &file_glutton_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{24} +} + +func (x *Request) GetKind() isRequest_Kind { + if x != nil { + return x.Kind + } + return nil +} + +func (x *Request) GetWriteRam() *WriteRAMRequest { + if x != nil { + if x, ok := x.Kind.(*Request_WriteRam); ok { + return x.WriteRam + } + } + return nil +} + +func (x *Request) GetReadRam() *ReadRAMRequest { + if x != nil { + if x, ok := x.Kind.(*Request_ReadRam); ok { + return x.ReadRam + } + } + return nil +} + +func (x *Request) GetWriteDisk() *WriteDiskRequest { + if x != nil { + if x, ok := x.Kind.(*Request_WriteDisk); ok { + return x.WriteDisk + } + } + return nil +} + +func (x *Request) GetReadDisk() *ReadDiskRequest { + if x != nil { + if x, ok := x.Kind.(*Request_ReadDisk); ok { + return x.ReadDisk + } + } + return nil +} + +func (x *Request) GetBurnCpu() *BurnCPURequest { + if x != nil { + if x, ok := x.Kind.(*Request_BurnCpu); ok { + return x.BurnCpu + } + } + return nil +} + +func (x *Request) GetIngest() *IngestRequest { + if x != nil { + if x, ok := x.Kind.(*Request_Ingest); ok { + return x.Ingest + } + } + return nil +} + +func (x *Request) GetSleep() *SleepRequest { + if x != nil { + if x, ok := x.Kind.(*Request_Sleep); ok { + return x.Sleep + } + } + return nil +} + +type isRequest_Kind interface { + isRequest_Kind() +} + +type Request_WriteRam struct { + WriteRam *WriteRAMRequest `protobuf:"bytes,1,opt,name=write_ram,json=writeRam,proto3,oneof"` +} + +type Request_ReadRam struct { + ReadRam *ReadRAMRequest `protobuf:"bytes,2,opt,name=read_ram,json=readRam,proto3,oneof"` +} + +type Request_WriteDisk struct { + WriteDisk *WriteDiskRequest `protobuf:"bytes,3,opt,name=write_disk,json=writeDisk,proto3,oneof"` +} + +type Request_ReadDisk struct { + ReadDisk *ReadDiskRequest `protobuf:"bytes,4,opt,name=read_disk,json=readDisk,proto3,oneof"` +} + +type Request_BurnCpu struct { + BurnCpu *BurnCPURequest `protobuf:"bytes,5,opt,name=burn_cpu,json=burnCpu,proto3,oneof"` +} + +type Request_Ingest struct { + Ingest *IngestRequest `protobuf:"bytes,6,opt,name=ingest,proto3,oneof"` +} + +type Request_Sleep struct { + Sleep *SleepRequest `protobuf:"bytes,7,opt,name=sleep,proto3,oneof"` +} + +func (*Request_WriteRam) isRequest_Kind() {} + +func (*Request_ReadRam) isRequest_Kind() {} + +func (*Request_WriteDisk) isRequest_Kind() {} + +func (*Request_ReadDisk) isRequest_Kind() {} + +func (*Request_BurnCpu) isRequest_Kind() {} + +func (*Request_Ingest) isRequest_Kind() {} + +func (*Request_Sleep) isRequest_Kind() {} + +type SleepRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Wall-clock time to idle, in milliseconds. The glutton stays awake and + // does nothing, which paces the requests around it. + DurationMs int64 `protobuf:"varint,1,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SleepRequest) Reset() { + *x = SleepRequest{} + mi := &file_glutton_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SleepRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SleepRequest) ProtoMessage() {} + +func (x *SleepRequest) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SleepRequest.ProtoReflect.Descriptor instead. +func (*SleepRequest) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{25} +} + +func (x *SleepRequest) GetDurationMs() int64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +type RunScriptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Script *Block `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunScriptRequest) Reset() { + *x = RunScriptRequest{} + mi := &file_glutton_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunScriptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunScriptRequest) ProtoMessage() {} + +func (x *RunScriptRequest) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunScriptRequest.ProtoReflect.Descriptor instead. +func (*RunScriptRequest) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{26} +} + +func (x *RunScriptRequest) GetScript() *Block { + if x != nil { + return x.Script + } + return nil +} + +type RunScriptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The script's root block. + Result *StepResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunScriptResponse) Reset() { + *x = RunScriptResponse{} + mi := &file_glutton_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunScriptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunScriptResponse) ProtoMessage() {} + +func (x *RunScriptResponse) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunScriptResponse.ProtoReflect.Descriptor instead. +func (*RunScriptResponse) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{27} +} + +func (x *RunScriptResponse) GetResult() *StepResult { + if x != nil { + return x.Result + } + return nil +} + +// A StepResult mirrors one node of the script: a block, an operation, or +// a request. Its children are in the request's order: a block's steps or +// an operation's requests; a request has none. A node inside a looped +// block is entered once per pass, and its stats sum over every entry, so +// the response is the size of the script however long it ran. +type StepResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stats *Stats `protobuf:"bytes,1,opt,name=stats,proto3" json:"stats,omitempty"` + Children []*StepResult `protobuf:"bytes,2,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StepResult) Reset() { + *x = StepResult{} + mi := &file_glutton_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StepResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StepResult) ProtoMessage() {} + +func (x *StepResult) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StepResult.ProtoReflect.Descriptor instead. +func (*StepResult) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{28} +} + +func (x *StepResult) GetStats() *Stats { + if x != nil { + return x.Stats + } + return nil +} + +func (x *StepResult) GetChildren() []*StepResult { + if x != nil { + return x.Children + } + return nil +} + +// Stats are a node's vital signs. Every field but elapsed_ms is the sum +// over the node's children and entries, so the root carries the totals. +type Stats struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Wall clock spent in this node, summed over its entries. A block's is + // the sum of its steps'; an operation's is its longest request's, since + // they overlap; so a parent's elapsed is not the sum of its children's. + ElapsedMs int64 `protobuf:"varint,1,opt,name=elapsed_ms,json=elapsedMs,proto3" json:"elapsed_ms,omitempty"` + // Times this node was entered: a looped block's pass count, and for + // every other node the entries its enclosing loops gave it. + Passes int64 `protobuf:"varint,2,opt,name=passes,proto3" json:"passes,omitempty"` + // Requests run beneath this node. + RequestsRun int64 `protobuf:"varint,3,opt,name=requests_run,json=requestsRun,proto3" json:"requests_run,omitempty"` + // Bytes WriteDisk generated and Ingest received, and bytes ReadDisk read. + DiskBytesWritten int64 `protobuf:"varint,4,opt,name=disk_bytes_written,json=diskBytesWritten,proto3" json:"disk_bytes_written,omitempty"` + DiskBytesRead int64 `protobuf:"varint,5,opt,name=disk_bytes_read,json=diskBytesRead,proto3" json:"disk_bytes_read,omitempty"` + // Bytes WriteRAM was asked to write, and bytes ReadRAM walked. + RamBytesWritten int64 `protobuf:"varint,6,opt,name=ram_bytes_written,json=ramBytesWritten,proto3" json:"ram_bytes_written,omitempty"` + RamBytesRead int64 `protobuf:"varint,7,opt,name=ram_bytes_read,json=ramBytesRead,proto3" json:"ram_bytes_read,omitempty"` + // Hash iterations BurnCPU completed. + BurnIterations int64 `protobuf:"varint,8,opt,name=burn_iterations,json=burnIterations,proto3" json:"burn_iterations,omitempty"` + // Milliseconds slept. + SleptMs int64 `protobuf:"varint,9,opt,name=slept_ms,json=sleptMs,proto3" json:"slept_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stats) Reset() { + *x = Stats{} + mi := &file_glutton_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stats) ProtoMessage() {} + +func (x *Stats) ProtoReflect() protoreflect.Message { + mi := &file_glutton_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stats.ProtoReflect.Descriptor instead. +func (*Stats) Descriptor() ([]byte, []int) { + return file_glutton_proto_rawDescGZIP(), []int{29} +} + +func (x *Stats) GetElapsedMs() int64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *Stats) GetPasses() int64 { + if x != nil { + return x.Passes + } + return 0 +} + +func (x *Stats) GetRequestsRun() int64 { + if x != nil { + return x.RequestsRun + } + return 0 +} + +func (x *Stats) GetDiskBytesWritten() int64 { + if x != nil { + return x.DiskBytesWritten + } + return 0 +} + +func (x *Stats) GetDiskBytesRead() int64 { + if x != nil { + return x.DiskBytesRead + } + return 0 +} + +func (x *Stats) GetRamBytesWritten() int64 { + if x != nil { + return x.RamBytesWritten + } + return 0 +} + +func (x *Stats) GetRamBytesRead() int64 { + if x != nil { + return x.RamBytesRead + } + return 0 +} + +func (x *Stats) GetBurnIterations() int64 { + if x != nil { + return x.BurnIterations + } + return 0 +} + +func (x *Stats) GetSleptMs() int64 { + if x != nil { + return x.SleptMs + } + return 0 +} + var File_glutton_proto protoreflect.FileDescriptor const file_glutton_proto_rawDesc = "" + @@ -1290,14 +1960,55 @@ const file_glutton_proto_rawDesc = "" + "\x11cap_at_gomaxprocs\x18\x04 \x01(\bH\x00R\x0fcapAtGomaxprocs\x88\x01\x01B\x14\n" + "\x12_cap_at_gomaxprocs\"-\n" + "\x0eUseCPUResponse\x12\x1b\n" + - "\tnum_cores\x18\x01 \x01(\x05R\bnumCores*_\n" + + "\tnum_cores\x18\x01 \x01(\x05R\bnumCores\"V\n" + + "\x05Block\x12#\n" + + "\x05steps\x18\x01 \x03(\v2\r.glutton.StepR\x05steps\x12(\n" + + "\x10loop_duration_ms\x18\x02 \x01(\x03R\x0eloopDurationMs\"j\n" + + "\x04Step\x12&\n" + + "\x05block\x18\x01 \x01(\v2\x0e.glutton.BlockH\x00R\x05block\x122\n" + + "\toperation\x18\x02 \x01(\v2\x12.glutton.OperationH\x00R\toperationB\x06\n" + + "\x04kind\"9\n" + + "\tOperation\x12,\n" + + "\brequests\x18\x01 \x03(\v2\x10.glutton.RequestR\brequests\"\x8c\x03\n" + + "\aRequest\x127\n" + + "\twrite_ram\x18\x01 \x01(\v2\x18.glutton.WriteRAMRequestH\x00R\bwriteRam\x124\n" + + "\bread_ram\x18\x02 \x01(\v2\x17.glutton.ReadRAMRequestH\x00R\areadRam\x12:\n" + + "\n" + + "write_disk\x18\x03 \x01(\v2\x19.glutton.WriteDiskRequestH\x00R\twriteDisk\x127\n" + + "\tread_disk\x18\x04 \x01(\v2\x18.glutton.ReadDiskRequestH\x00R\breadDisk\x124\n" + + "\bburn_cpu\x18\x05 \x01(\v2\x17.glutton.BurnCPURequestH\x00R\aburnCpu\x120\n" + + "\x06ingest\x18\x06 \x01(\v2\x16.glutton.IngestRequestH\x00R\x06ingest\x12-\n" + + "\x05sleep\x18\a \x01(\v2\x15.glutton.SleepRequestH\x00R\x05sleepB\x06\n" + + "\x04kind\"/\n" + + "\fSleepRequest\x12\x1f\n" + + "\vduration_ms\x18\x01 \x01(\x03R\n" + + "durationMs\":\n" + + "\x10RunScriptRequest\x12&\n" + + "\x06script\x18\x01 \x01(\v2\x0e.glutton.BlockR\x06script\"@\n" + + "\x11RunScriptResponse\x12+\n" + + "\x06result\x18\x01 \x01(\v2\x13.glutton.StepResultR\x06result\"c\n" + + "\n" + + "StepResult\x12$\n" + + "\x05stats\x18\x01 \x01(\v2\x0e.glutton.StatsR\x05stats\x12/\n" + + "\bchildren\x18\x02 \x03(\v2\x13.glutton.StepResultR\bchildren\"\xcd\x02\n" + + "\x05Stats\x12\x1d\n" + + "\n" + + "elapsed_ms\x18\x01 \x01(\x03R\telapsedMs\x12\x16\n" + + "\x06passes\x18\x02 \x01(\x03R\x06passes\x12!\n" + + "\frequests_run\x18\x03 \x01(\x03R\vrequestsRun\x12,\n" + + "\x12disk_bytes_written\x18\x04 \x01(\x03R\x10diskBytesWritten\x12&\n" + + "\x0fdisk_bytes_read\x18\x05 \x01(\x03R\rdiskBytesRead\x12*\n" + + "\x11ram_bytes_written\x18\x06 \x01(\x03R\x0framBytesWritten\x12$\n" + + "\x0eram_bytes_read\x18\a \x01(\x03R\framBytesRead\x12'\n" + + "\x0fburn_iterations\x18\b \x01(\x03R\x0eburnIterations\x12\x19\n" + + "\bslept_ms\x18\t \x01(\x03R\asleptMs*_\n" + "\tWriteMode\x12\x17\n" + "\x13WRITE_MODE_TRUNCATE\x10\x00\x12\x18\n" + "\x14WRITE_MODE_OVERWRITE\x10\x01\x12\x1f\n" + "\x1bWRITE_MODE_OVERWRITE_ROTATE\x10\x02*9\n" + "\bReadMode\x12\x12\n" + "\x0eREAD_MODE_DATA\x10\x00\x12\x19\n" + - "\x15READ_MODE_DIGEST_ONLY\x10\x012\x80\x05\n" + + "\x15READ_MODE_DIGEST_ONLY\x10\x012\xc6\x05\n" + "\aGlutton\x12A\n" + "\bWriteRAM\x12\x18.glutton.WriteRAMRequest\x1a\x19.glutton.WriteRAMResponse\"\x00\x12>\n" + "\aReadRAM\x12\x17.glutton.ReadRAMRequest\x1a\x18.glutton.ReadRAMResponse\"\x00\x12D\n" + @@ -1308,7 +2019,8 @@ const file_glutton_proto_rawDesc = "" + "\x06Gossip\x12\x16.glutton.GossipRequest\x1a\x17.glutton.GossipResponse\"\x00\x12>\n" + "\aBurnCPU\x12\x17.glutton.BurnCPURequest\x1a\x18.glutton.BurnCPUResponse\"\x00\x12;\n" + "\x06Ingest\x12\x16.glutton.IngestRequest\x1a\x17.glutton.IngestResponse\"\x00\x12;\n" + - "\x06UseCPU\x12\x16.glutton.UseCPURequest\x1a\x17.glutton.UseCPUResponse\"\x00B=Z;github.com/agent-substrate/substrate/internal/proto/gluttonb\x06proto3" + "\x06UseCPU\x12\x16.glutton.UseCPURequest\x1a\x17.glutton.UseCPUResponse\"\x00\x12D\n" + + "\tRunScript\x12\x19.glutton.RunScriptRequest\x1a\x1a.glutton.RunScriptResponse\"\x00B=Z;github.com/agent-substrate/substrate/internal/proto/gluttonb\x06proto3" var ( file_glutton_proto_rawDescOnce sync.Once @@ -1323,7 +2035,7 @@ func file_glutton_proto_rawDescGZIP() []byte { } var file_glutton_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_glutton_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_glutton_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_glutton_proto_goTypes = []any{ (WriteMode)(0), // 0: glutton.WriteMode (ReadMode)(0), // 1: glutton.ReadMode @@ -1348,37 +2060,63 @@ var file_glutton_proto_goTypes = []any{ (*IngestResponse)(nil), // 20: glutton.IngestResponse (*UseCPURequest)(nil), // 21: glutton.UseCPURequest (*UseCPUResponse)(nil), // 22: glutton.UseCPUResponse + (*Block)(nil), // 23: glutton.Block + (*Step)(nil), // 24: glutton.Step + (*Operation)(nil), // 25: glutton.Operation + (*Request)(nil), // 26: glutton.Request + (*SleepRequest)(nil), // 27: glutton.SleepRequest + (*RunScriptRequest)(nil), // 28: glutton.RunScriptRequest + (*RunScriptResponse)(nil), // 29: glutton.RunScriptResponse + (*StepResult)(nil), // 30: glutton.StepResult + (*Stats)(nil), // 31: glutton.Stats } var file_glutton_proto_depIdxs = []int32{ 0, // 0: glutton.WriteRAMRequest.write_mode:type_name -> glutton.WriteMode 0, // 1: glutton.WriteDiskRequest.write_mode:type_name -> glutton.WriteMode 1, // 2: glutton.ReadDiskRequest.read_mode:type_name -> glutton.ReadMode 16, // 3: glutton.GossipRequest.peers:type_name -> glutton.Peer - 2, // 4: glutton.Glutton.WriteRAM:input_type -> glutton.WriteRAMRequest - 4, // 5: glutton.Glutton.ReadRAM:input_type -> glutton.ReadRAMRequest - 6, // 6: glutton.Glutton.WriteDisk:input_type -> glutton.WriteDiskRequest - 8, // 7: glutton.Glutton.ReadDisk:input_type -> glutton.ReadDiskRequest - 10, // 8: glutton.Glutton.OpenFD:input_type -> glutton.OpenFDRequest - 12, // 9: glutton.Glutton.Ping:input_type -> glutton.PingRequest - 14, // 10: glutton.Glutton.Gossip:input_type -> glutton.GossipRequest - 17, // 11: glutton.Glutton.BurnCPU:input_type -> glutton.BurnCPURequest - 19, // 12: glutton.Glutton.Ingest:input_type -> glutton.IngestRequest - 21, // 13: glutton.Glutton.UseCPU:input_type -> glutton.UseCPURequest - 3, // 14: glutton.Glutton.WriteRAM:output_type -> glutton.WriteRAMResponse - 5, // 15: glutton.Glutton.ReadRAM:output_type -> glutton.ReadRAMResponse - 7, // 16: glutton.Glutton.WriteDisk:output_type -> glutton.WriteDiskResponse - 9, // 17: glutton.Glutton.ReadDisk:output_type -> glutton.ReadDiskResponse - 11, // 18: glutton.Glutton.OpenFD:output_type -> glutton.OpenFDResponse - 13, // 19: glutton.Glutton.Ping:output_type -> glutton.PingResponse - 15, // 20: glutton.Glutton.Gossip:output_type -> glutton.GossipResponse - 18, // 21: glutton.Glutton.BurnCPU:output_type -> glutton.BurnCPUResponse - 20, // 22: glutton.Glutton.Ingest:output_type -> glutton.IngestResponse - 22, // 23: glutton.Glutton.UseCPU:output_type -> glutton.UseCPUResponse - 14, // [14:24] is the sub-list for method output_type - 4, // [4:14] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 24, // 4: glutton.Block.steps:type_name -> glutton.Step + 23, // 5: glutton.Step.block:type_name -> glutton.Block + 25, // 6: glutton.Step.operation:type_name -> glutton.Operation + 26, // 7: glutton.Operation.requests:type_name -> glutton.Request + 2, // 8: glutton.Request.write_ram:type_name -> glutton.WriteRAMRequest + 4, // 9: glutton.Request.read_ram:type_name -> glutton.ReadRAMRequest + 6, // 10: glutton.Request.write_disk:type_name -> glutton.WriteDiskRequest + 8, // 11: glutton.Request.read_disk:type_name -> glutton.ReadDiskRequest + 17, // 12: glutton.Request.burn_cpu:type_name -> glutton.BurnCPURequest + 19, // 13: glutton.Request.ingest:type_name -> glutton.IngestRequest + 27, // 14: glutton.Request.sleep:type_name -> glutton.SleepRequest + 23, // 15: glutton.RunScriptRequest.script:type_name -> glutton.Block + 30, // 16: glutton.RunScriptResponse.result:type_name -> glutton.StepResult + 31, // 17: glutton.StepResult.stats:type_name -> glutton.Stats + 30, // 18: glutton.StepResult.children:type_name -> glutton.StepResult + 2, // 19: glutton.Glutton.WriteRAM:input_type -> glutton.WriteRAMRequest + 4, // 20: glutton.Glutton.ReadRAM:input_type -> glutton.ReadRAMRequest + 6, // 21: glutton.Glutton.WriteDisk:input_type -> glutton.WriteDiskRequest + 8, // 22: glutton.Glutton.ReadDisk:input_type -> glutton.ReadDiskRequest + 10, // 23: glutton.Glutton.OpenFD:input_type -> glutton.OpenFDRequest + 12, // 24: glutton.Glutton.Ping:input_type -> glutton.PingRequest + 14, // 25: glutton.Glutton.Gossip:input_type -> glutton.GossipRequest + 17, // 26: glutton.Glutton.BurnCPU:input_type -> glutton.BurnCPURequest + 19, // 27: glutton.Glutton.Ingest:input_type -> glutton.IngestRequest + 21, // 28: glutton.Glutton.UseCPU:input_type -> glutton.UseCPURequest + 28, // 29: glutton.Glutton.RunScript:input_type -> glutton.RunScriptRequest + 3, // 30: glutton.Glutton.WriteRAM:output_type -> glutton.WriteRAMResponse + 5, // 31: glutton.Glutton.ReadRAM:output_type -> glutton.ReadRAMResponse + 7, // 32: glutton.Glutton.WriteDisk:output_type -> glutton.WriteDiskResponse + 9, // 33: glutton.Glutton.ReadDisk:output_type -> glutton.ReadDiskResponse + 11, // 34: glutton.Glutton.OpenFD:output_type -> glutton.OpenFDResponse + 13, // 35: glutton.Glutton.Ping:output_type -> glutton.PingResponse + 15, // 36: glutton.Glutton.Gossip:output_type -> glutton.GossipResponse + 18, // 37: glutton.Glutton.BurnCPU:output_type -> glutton.BurnCPUResponse + 20, // 38: glutton.Glutton.Ingest:output_type -> glutton.IngestResponse + 22, // 39: glutton.Glutton.UseCPU:output_type -> glutton.UseCPUResponse + 29, // 40: glutton.Glutton.RunScript:output_type -> glutton.RunScriptResponse + 30, // [30:41] is the sub-list for method output_type + 19, // [19:30] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_glutton_proto_init() } @@ -1387,13 +2125,26 @@ func file_glutton_proto_init() { return } file_glutton_proto_msgTypes[19].OneofWrappers = []any{} + file_glutton_proto_msgTypes[22].OneofWrappers = []any{ + (*Step_Block)(nil), + (*Step_Operation)(nil), + } + file_glutton_proto_msgTypes[24].OneofWrappers = []any{ + (*Request_WriteRam)(nil), + (*Request_ReadRam)(nil), + (*Request_WriteDisk)(nil), + (*Request_ReadDisk)(nil), + (*Request_BurnCpu)(nil), + (*Request_Ingest)(nil), + (*Request_Sleep)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_glutton_proto_rawDesc), len(file_glutton_proto_rawDesc)), NumEnums: 2, - NumMessages: 21, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/glutton/glutton.proto b/internal/proto/glutton/glutton.proto index 1e278574f3..25b9da13c3 100644 --- a/internal/proto/glutton/glutton.proto +++ b/internal/proto/glutton/glutton.proto @@ -66,6 +66,16 @@ service Glutton { // load; calling again replaces it, and num_cores=0 stops it. See // UseCPURequest for the (goroutines x duty cycle) shape. rpc UseCPU(UseCPURequest) returns (UseCPUResponse) {} + + // Runs a script inside the glutton as one request, so a caller can act + // out a whole on-sandbox activity (a build, a test run, a file churn) + // without a network round trip per operation. A script is a tree: a + // Block runs its steps in order and may loop them for a wall-clock + // budget; an Operation runs its requests at the same time. The request + // holds the connection open until the script finishes, and the response + // mirrors the tree with summed stats at every node; the requests' own + // results are not returned. + rpc RunScript(RunScriptRequest) returns (RunScriptResponse) {} } enum WriteMode { @@ -244,3 +254,105 @@ message UseCPUResponse { // num_cores unless it was capped at GOMAXPROCS. int32 num_cores = 1; } + +// A Block is a list of steps run in order. With loop_duration_ms set, the +// block cycles through its steps until the budget elapses. A budget is +// checked between passes: the pass in progress finishes, so a loop +// overruns its budget by at most one pass and no request is cut short. +// A nested block stops cycling once any ancestor's budget has elapsed, +// the same way. +message Block { + // At least one step is required. + repeated Step steps = 1; + + // Wall-clock budget to cycle through the steps for, in milliseconds; + // the pass under way when it elapses finishes. Zero runs the steps once. + int64 loop_duration_ms = 2; +} + +// A Step is either a nested Block or an Operation. +message Step { + oneof kind { + Block block = 1; + Operation operation = 2; + } +} + +// An Operation runs its requests at the same time and waits for all of +// them. The first request to fail cancels the rest. RAM requests take +// turns on the glutton's single RAM lock, so only CPU, disk, and sleep +// requests overlap in practice. +message Operation { + // At least one request is required. + repeated Request requests = 1; +} + +// A Request is one of the service's work requests. UseCPU and OpenFD are +// left out because they set a background load that outlives the script +// (BurnCPU is the CPU spike), and Gossip because it starts a loop. +message Request { + oneof kind { + WriteRAMRequest write_ram = 1; + ReadRAMRequest read_ram = 2; + WriteDiskRequest write_disk = 3; + ReadDiskRequest read_disk = 4; + BurnCPURequest burn_cpu = 5; + IngestRequest ingest = 6; + SleepRequest sleep = 7; + } +} + +message SleepRequest { + // Wall-clock time to idle, in milliseconds. The glutton stays awake and + // does nothing, which paces the requests around it. + int64 duration_ms = 1; +} + +message RunScriptRequest { + Block script = 1; +} + +message RunScriptResponse { + // The script's root block. + StepResult result = 1; +} + +// A StepResult mirrors one node of the script: a block, an operation, or +// a request. Its children are in the request's order: a block's steps or +// an operation's requests; a request has none. A node inside a looped +// block is entered once per pass, and its stats sum over every entry, so +// the response is the size of the script however long it ran. +message StepResult { + Stats stats = 1; + repeated StepResult children = 2; +} + +// Stats are a node's vital signs. Every field but elapsed_ms is the sum +// over the node's children and entries, so the root carries the totals. +message Stats { + // Wall clock spent in this node, summed over its entries. A block's is + // the sum of its steps'; an operation's is its longest request's, since + // they overlap; so a parent's elapsed is not the sum of its children's. + int64 elapsed_ms = 1; + + // Times this node was entered: a looped block's pass count, and for + // every other node the entries its enclosing loops gave it. + int64 passes = 2; + + // Requests run beneath this node. + int64 requests_run = 3; + + // Bytes WriteDisk generated and Ingest received, and bytes ReadDisk read. + int64 disk_bytes_written = 4; + int64 disk_bytes_read = 5; + + // Bytes WriteRAM was asked to write, and bytes ReadRAM walked. + int64 ram_bytes_written = 6; + int64 ram_bytes_read = 7; + + // Hash iterations BurnCPU completed. + int64 burn_iterations = 8; + + // Milliseconds slept. + int64 slept_ms = 9; +} diff --git a/internal/proto/glutton/glutton_grpc.pb.go b/internal/proto/glutton/glutton_grpc.pb.go index f6e590b4b1..5952f01960 100644 --- a/internal/proto/glutton/glutton_grpc.pb.go +++ b/internal/proto/glutton/glutton_grpc.pb.go @@ -43,6 +43,7 @@ const ( Glutton_BurnCPU_FullMethodName = "/glutton.Glutton/BurnCPU" Glutton_Ingest_FullMethodName = "/glutton.Glutton/Ingest" Glutton_UseCPU_FullMethodName = "/glutton.Glutton/UseCPU" + Glutton_RunScript_FullMethodName = "/glutton.Glutton/RunScript" ) // GluttonClient is the client API for Glutton service. @@ -88,6 +89,15 @@ type GluttonClient interface { // load; calling again replaces it, and num_cores=0 stops it. See // UseCPURequest for the (goroutines x duty cycle) shape. UseCPU(ctx context.Context, in *UseCPURequest, opts ...grpc.CallOption) (*UseCPUResponse, error) + // Runs a script inside the glutton as one request, so a caller can act + // out a whole on-sandbox activity (a build, a test run, a file churn) + // without a network round trip per operation. A script is a tree: a + // Block runs its steps in order and may loop them for a wall-clock + // budget; an Operation runs its requests at the same time. The request + // holds the connection open until the script finishes, and the response + // mirrors the tree with summed stats at every node; the requests' own + // results are not returned. + RunScript(ctx context.Context, in *RunScriptRequest, opts ...grpc.CallOption) (*RunScriptResponse, error) } type gluttonClient struct { @@ -198,6 +208,16 @@ func (c *gluttonClient) UseCPU(ctx context.Context, in *UseCPURequest, opts ...g return out, nil } +func (c *gluttonClient) RunScript(ctx context.Context, in *RunScriptRequest, opts ...grpc.CallOption) (*RunScriptResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RunScriptResponse) + err := c.cc.Invoke(ctx, Glutton_RunScript_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // GluttonServer is the server API for Glutton service. // All implementations must embed UnimplementedGluttonServer // for forward compatibility. @@ -241,6 +261,15 @@ type GluttonServer interface { // load; calling again replaces it, and num_cores=0 stops it. See // UseCPURequest for the (goroutines x duty cycle) shape. UseCPU(context.Context, *UseCPURequest) (*UseCPUResponse, error) + // Runs a script inside the glutton as one request, so a caller can act + // out a whole on-sandbox activity (a build, a test run, a file churn) + // without a network round trip per operation. A script is a tree: a + // Block runs its steps in order and may loop them for a wall-clock + // budget; an Operation runs its requests at the same time. The request + // holds the connection open until the script finishes, and the response + // mirrors the tree with summed stats at every node; the requests' own + // results are not returned. + RunScript(context.Context, *RunScriptRequest) (*RunScriptResponse, error) mustEmbedUnimplementedGluttonServer() } @@ -281,6 +310,9 @@ func (UnimplementedGluttonServer) Ingest(context.Context, *IngestRequest) (*Inge func (UnimplementedGluttonServer) UseCPU(context.Context, *UseCPURequest) (*UseCPUResponse, error) { return nil, status.Error(codes.Unimplemented, "method UseCPU not implemented") } +func (UnimplementedGluttonServer) RunScript(context.Context, *RunScriptRequest) (*RunScriptResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RunScript not implemented") +} func (UnimplementedGluttonServer) mustEmbedUnimplementedGluttonServer() {} func (UnimplementedGluttonServer) testEmbeddedByValue() {} @@ -482,6 +514,24 @@ func _Glutton_UseCPU_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _Glutton_RunScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunScriptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GluttonServer).RunScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Glutton_RunScript_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GluttonServer).RunScript(ctx, req.(*RunScriptRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Glutton_ServiceDesc is the grpc.ServiceDesc for Glutton service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -529,6 +579,10 @@ var Glutton_ServiceDesc = grpc.ServiceDesc{ MethodName: "UseCPU", Handler: _Glutton_UseCPU_Handler, }, + { + MethodName: "RunScript", + Handler: _Glutton_RunScript_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "glutton.proto",