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",