fix: harden stage execution failures
This commit is contained in:
parent
0a5947f45e
commit
edbdbd41a8
|
|
@ -104,6 +104,7 @@ func run(conf *joj3Conf.Conf, conventionalCommit *joj3Conf.ConventionalCommit) e
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("stage run", "error", err)
|
slog.Error("stage run", "error", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if forceQuitStageName != "" {
|
if forceQuitStageName != "" {
|
||||||
slog.Info("stage force quit", "name", forceQuitStageName)
|
slog.Info("stage force quit", "name", forceQuitStageName)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -124,6 +126,7 @@ func newErrorStageResults(err error) ([]stage.StageResult, string) {
|
||||||
}, "Internal Error"
|
}, "Internal Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:unparam // named err lets deferred cleanup errors reach the caller
|
||||||
func runStages(
|
func runStages(
|
||||||
conf *conf.Conf,
|
conf *conf.Conf,
|
||||||
groups []string,
|
groups []string,
|
||||||
|
|
@ -153,36 +156,42 @@ func runStages(
|
||||||
stageResults, forceQuitStageName = newErrorStageResults(err)
|
stageResults, forceQuitStageName = newErrorStageResults(err)
|
||||||
return stageResults, forceQuitStageName, err
|
return stageResults, forceQuitStageName, err
|
||||||
}
|
}
|
||||||
defer stage.Cleanup()
|
ctx := context.Background()
|
||||||
|
defer func() {
|
||||||
|
err = errors.Join(err, stage.Cleanup(ctx))
|
||||||
|
}()
|
||||||
// ignore force quit in preStages & postStages
|
// ignore force quit in preStages & postStages
|
||||||
slog.Info("run preStages")
|
slog.Info("run preStages")
|
||||||
_, _, err = stage.Run(preStages)
|
_, _, preErr := stage.Run(ctx, preStages)
|
||||||
if err != nil {
|
if preErr != nil {
|
||||||
slog.Error("run preStages", "error", err)
|
slog.Error("run preStages", "error", preErr)
|
||||||
}
|
}
|
||||||
slog.Info("run stages")
|
slog.Info("run stages")
|
||||||
stageResults, forceQuitStageName, err = stage.Run(stages)
|
stageResults, forceQuitStageName, mainErr := stage.Run(ctx, stages)
|
||||||
if err != nil {
|
if mainErr != nil {
|
||||||
slog.Error("run stages", "error", err)
|
slog.Error("run stages", "error", mainErr)
|
||||||
stageResults, forceQuitStageName = newErrorStageResults(err)
|
stageResults, forceQuitStageName = newErrorStageResults(mainErr)
|
||||||
}
|
}
|
||||||
onStagesComplete(stageResults, forceQuitStageName)
|
onStagesComplete(stageResults, forceQuitStageName)
|
||||||
slog.Info("output result start", "path", conf.OutputPath)
|
slog.Info("output result start", "path", conf.OutputPath)
|
||||||
slog.Debug("output result start",
|
slog.Debug("output result start",
|
||||||
"path", conf.OutputPath, "results", stageResults)
|
"path", conf.OutputPath, "results", stageResults)
|
||||||
content, err := json.Marshal(stageResults)
|
content, marshalErr := json.Marshal(stageResults)
|
||||||
if err != nil {
|
if marshalErr != nil {
|
||||||
slog.Error("marshal stageResults", "error", err)
|
slog.Error("marshal stageResults", "error", marshalErr)
|
||||||
}
|
}
|
||||||
err = os.WriteFile(conf.OutputPath,
|
var outputErr error
|
||||||
append(content, []byte("\n")...), 0o600)
|
if marshalErr == nil {
|
||||||
if err != nil {
|
outputErr = os.WriteFile(conf.OutputPath,
|
||||||
slog.Error("write stageResults", "error", err)
|
append(content, '\n'), 0o600)
|
||||||
|
if outputErr != nil {
|
||||||
|
slog.Error("write stageResults", "error", outputErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
slog.Info("run postStages")
|
slog.Info("run postStages")
|
||||||
_, _, err = stage.Run(postStages)
|
_, _, postErr := stage.Run(ctx, postStages)
|
||||||
if err != nil {
|
if postErr != nil {
|
||||||
slog.Error("run postStages", "error", err)
|
slog.Error("run postStages", "error", postErr)
|
||||||
}
|
}
|
||||||
return stageResults, forceQuitStageName, err
|
return stageResults, forceQuitStageName, errors.Join(preErr, mainErr, marshalErr, outputErr, postErr)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
cmd/joj3/stage_test.go
Normal file
27
cmd/joj3/stage_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joint-online-judge/JOJ3/cmd/joj3/conf"
|
||||||
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunStagesPreservesMainAndOutputErrors(t *testing.T) {
|
||||||
|
brokenStage := conf.ConfStage{Name: "broken"}
|
||||||
|
brokenStage.Executor.Name = "missing-executor"
|
||||||
|
c := &conf.Conf{
|
||||||
|
SandboxExecServer: "localhost:5051",
|
||||||
|
OutputPath: t.TempDir(),
|
||||||
|
Stages: []conf.ConfStage{brokenStage},
|
||||||
|
}
|
||||||
|
_, _, err := runStages(c, nil, func([]stage.StageResult, string) {})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("runStages() unexpectedly succeeded")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "executor not found") ||
|
||||||
|
!strings.Contains(err.Error(), "is a directory") {
|
||||||
|
t.Fatalf("runStages() error = %v, want main and output errors", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package dummy
|
package dummy
|
||||||
|
|
||||||
import "github.com/joint-online-judge/JOJ3/internal/stage"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
func (e *Dummy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Dummy) Run(_ context.Context, cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
res := make([]stage.ExecutorResult, 0, len(cmds))
|
res := make([]stage.ExecutorResult, 0, len(cmds))
|
||||||
for range cmds {
|
for range cmds {
|
||||||
res = append(res, stage.ExecutorResult{
|
res = append(res, stage.ExecutorResult{
|
||||||
|
|
@ -19,6 +23,6 @@ func (e *Dummy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Dummy) Cleanup() error {
|
func (e *Dummy) Cleanup(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
|
@ -105,10 +106,16 @@ func (e *Local) generateResult(
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Local) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
func (e *Local) Run(ctx context.Context, cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
var results []stage.ExecutorResult
|
var results []stage.ExecutorResult
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
|
if len(cmd.Args) == 0 {
|
||||||
|
return nil, fmt.Errorf("command args must not be empty")
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
execCmd := exec.Command(cmd.Args[0], cmd.Args[1:]...) // #nosec G204
|
execCmd := exec.Command(cmd.Args[0], cmd.Args[1:]...) // #nosec G204
|
||||||
execCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
execCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
if cmd.CPULimit > 0 && cmd.ClockLimit <= 0 {
|
if cmd.CPULimit > 0 && cmd.ClockLimit <= 0 {
|
||||||
|
|
@ -167,6 +174,10 @@ func (e *Local) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
|
case <-ctx.Done():
|
||||||
|
_ = syscall.Kill(-execCmd.Process.Pid, syscall.SIGKILL)
|
||||||
|
<-done
|
||||||
|
return nil, ctx.Err()
|
||||||
case <-time.After(duration):
|
case <-time.After(duration):
|
||||||
_ = syscall.Kill(-execCmd.Process.Pid, syscall.SIGKILL)
|
_ = syscall.Kill(-execCmd.Process.Pid, syscall.SIGKILL)
|
||||||
err := <-done
|
err := <-done
|
||||||
|
|
@ -216,6 +227,6 @@ func handleCopyOut(result *stage.ExecutorResult, cmd stage.Cmd) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Local) Cleanup() error {
|
func (e *Local) Cleanup(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -12,14 +12,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// copied from https://github.com/criyle/go-judge/blob/master/cmd/go-judge-shell/grpc.go
|
// copied from https://github.com/criyle/go-judge/blob/master/cmd/go-judge-shell/grpc.go
|
||||||
func convertPBCmd(cmd []stage.Cmd) []*pb.Request_CmdType {
|
func convertPBCmd(cmd []stage.Cmd) ([]*pb.Request_CmdType, error) {
|
||||||
ret := make([]*pb.Request_CmdType, 0, len(cmd))
|
ret := make([]*pb.Request_CmdType, 0, len(cmd))
|
||||||
for _, c := range cmd {
|
for index, c := range cmd {
|
||||||
|
files, err := convertPBFiles([]*stage.CmdFile{c.Stdin, c.Stdout, c.Stderr})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("command %d standard file: %w", index, err)
|
||||||
|
}
|
||||||
|
copyIn, err := convertPBCopyIn(c.CopyIn, c.CopyInDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("command %d copy-in: %w", index, err)
|
||||||
|
}
|
||||||
req := &pb.Request_CmdType{}
|
req := &pb.Request_CmdType{}
|
||||||
req.SetArgs(c.Args)
|
req.SetArgs(c.Args)
|
||||||
req.SetEnv(c.Env)
|
req.SetEnv(c.Env)
|
||||||
req.SetTty(c.TTY)
|
req.SetTty(c.TTY)
|
||||||
req.SetFiles(convertPBFiles([]*stage.CmdFile{c.Stdin, c.Stdout, c.Stderr}))
|
req.SetFiles(files)
|
||||||
req.SetCpuTimeLimit(c.CPULimit)
|
req.SetCpuTimeLimit(c.CPULimit)
|
||||||
req.SetClockTimeLimit(c.ClockLimit)
|
req.SetClockTimeLimit(c.ClockLimit)
|
||||||
req.SetMemoryLimit(c.MemoryLimit)
|
req.SetMemoryLimit(c.MemoryLimit)
|
||||||
|
|
@ -29,7 +37,7 @@ func convertPBCmd(cmd []stage.Cmd) []*pb.Request_CmdType {
|
||||||
req.SetCpuSetLimit(c.CPUSetLimit)
|
req.SetCpuSetLimit(c.CPUSetLimit)
|
||||||
req.SetDataSegmentLimit(c.DataSegmentLimit)
|
req.SetDataSegmentLimit(c.DataSegmentLimit)
|
||||||
req.SetAddressSpaceLimit(c.AddressSpaceLimit)
|
req.SetAddressSpaceLimit(c.AddressSpaceLimit)
|
||||||
req.SetCopyIn(convertPBCopyIn(c.CopyIn, c.CopyInDir))
|
req.SetCopyIn(copyIn)
|
||||||
req.SetCopyOut(convertPBCopyOut(c.CopyOut))
|
req.SetCopyOut(convertPBCopyOut(c.CopyOut))
|
||||||
req.SetCopyOutCached(convertPBCopyOut(c.CopyOutCached))
|
req.SetCopyOutCached(convertPBCopyOut(c.CopyOutCached))
|
||||||
req.SetCopyOutMax(c.CopyOutMax)
|
req.SetCopyOutMax(c.CopyOutMax)
|
||||||
|
|
@ -37,25 +45,25 @@ func convertPBCmd(cmd []stage.Cmd) []*pb.Request_CmdType {
|
||||||
req.SetSymlinks(convertSymlink(c.CopyIn))
|
req.SetSymlinks(convertSymlink(c.CopyIn))
|
||||||
ret = append(ret, req)
|
ret = append(ret, req)
|
||||||
}
|
}
|
||||||
return ret
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPBCopyIn(
|
func convertPBCopyIn(
|
||||||
copyIn map[string]stage.CmdFile, copyInDir string,
|
copyIn map[string]stage.CmdFile, copyInDir string,
|
||||||
) map[string]*pb.Request_File {
|
) (map[string]*pb.Request_File, error) {
|
||||||
if copyInDir != "" {
|
if copyInDir != "" {
|
||||||
_ = filepath.Walk(copyInDir,
|
err := filepath.Walk(copyInDir,
|
||||||
func(path string, info os.FileInfo, err error) error {
|
func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
absPath, err := filepath.Abs(path)
|
absPath, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
relPath, err := filepath.Rel(copyInDir, path)
|
relPath, err := filepath.Rel(copyInDir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
_, exists := copyIn[relPath]
|
_, exists := copyIn[relPath]
|
||||||
if !info.IsDir() && !exists {
|
if !info.IsDir() && !exists {
|
||||||
|
|
@ -63,15 +71,22 @@ func convertPBCopyIn(
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("walk %q: %w", copyInDir, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rt := make(map[string]*pb.Request_File, len(copyIn))
|
rt := make(map[string]*pb.Request_File, len(copyIn))
|
||||||
for k, i := range copyIn {
|
for k, i := range copyIn {
|
||||||
if i.Symlink != nil {
|
if i.Symlink != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rt[k] = convertPBFile(i)
|
file, err := convertPBFile(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("file %q: %w", k, err)
|
||||||
|
}
|
||||||
|
rt[k] = file
|
||||||
}
|
}
|
||||||
return rt
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPBCopyOut(copyOut []string) []*pb.Request_CmdCopyOutFile {
|
func convertPBCopyOut(copyOut []string) []*pb.Request_CmdCopyOutFile {
|
||||||
|
|
@ -101,65 +116,67 @@ func convertSymlink(copyIn map[string]stage.CmdFile) map[string]string {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPBFiles(files []*stage.CmdFile) []*pb.Request_File {
|
func convertPBFiles(files []*stage.CmdFile) ([]*pb.Request_File, error) {
|
||||||
var ret []*pb.Request_File
|
var ret []*pb.Request_File
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f == nil {
|
if f == nil {
|
||||||
ret = append(ret, nil)
|
ret = append(ret, nil)
|
||||||
} else {
|
} else {
|
||||||
ret = append(ret, convertPBFile(*f))
|
file, err := convertPBFile(*f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret = append(ret, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPBFile(i stage.CmdFile) *pb.Request_File {
|
func convertPBFile(i stage.CmdFile) (*pb.Request_File, error) {
|
||||||
req := &pb.Request_File{}
|
req := &pb.Request_File{}
|
||||||
switch {
|
switch {
|
||||||
case i.Src != nil:
|
case i.Src != nil:
|
||||||
if !filepath.IsAbs(*i.Src) {
|
if !filepath.IsAbs(*i.Src) {
|
||||||
absPath, err := filepath.Abs(*i.Src)
|
absPath, err := filepath.Abs(*i.Src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("convert pb file get abs path", "path", *i.Src, "error", err)
|
return nil, fmt.Errorf("resolve source path %q: %w", *i.Src, err)
|
||||||
absPath = "/"
|
|
||||||
}
|
}
|
||||||
i.Src = &absPath
|
i.Src = &absPath
|
||||||
}
|
}
|
||||||
s, err := os.ReadFile(*i.Src)
|
s, err := os.ReadFile(*i.Src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s = []byte{}
|
return nil, fmt.Errorf("read source file %q: %w", *i.Src, err)
|
||||||
slog.Error("convert pb file read file", "path", *i.Src, "error", err)
|
|
||||||
}
|
}
|
||||||
m := &pb.Request_MemoryFile{}
|
m := &pb.Request_MemoryFile{}
|
||||||
m.SetContent(s)
|
m.SetContent(s)
|
||||||
req.SetMemory(m)
|
req.SetMemory(m)
|
||||||
return req
|
return req, nil
|
||||||
case i.Content != nil:
|
case i.Content != nil:
|
||||||
s := strToBytes(*i.Content)
|
s := strToBytes(*i.Content)
|
||||||
m := &pb.Request_MemoryFile{}
|
m := &pb.Request_MemoryFile{}
|
||||||
m.SetContent(s)
|
m.SetContent(s)
|
||||||
req.SetMemory(m)
|
req.SetMemory(m)
|
||||||
return req
|
return req, nil
|
||||||
case i.FileID != nil:
|
case i.FileID != nil:
|
||||||
c := &pb.Request_CachedFile{}
|
c := &pb.Request_CachedFile{}
|
||||||
c.SetFileID(*i.FileID)
|
c.SetFileID(*i.FileID)
|
||||||
req.SetCached(c)
|
req.SetCached(c)
|
||||||
return req
|
return req, nil
|
||||||
case i.Name != nil && i.Max != nil:
|
case i.Name != nil && i.Max != nil:
|
||||||
p := &pb.Request_PipeCollector{}
|
p := &pb.Request_PipeCollector{}
|
||||||
p.SetName(*i.Name)
|
p.SetName(*i.Name)
|
||||||
p.SetMax(*i.Max)
|
p.SetMax(*i.Max)
|
||||||
p.SetPipe(i.Pipe)
|
p.SetPipe(i.Pipe)
|
||||||
req.SetPipe(p)
|
req.SetPipe(p)
|
||||||
return req
|
return req, nil
|
||||||
case i.StreamIn:
|
case i.StreamIn:
|
||||||
req.SetStreamIn(&emptypb.Empty{})
|
req.SetStreamIn(&emptypb.Empty{})
|
||||||
return req
|
return req, nil
|
||||||
case i.StreamOut:
|
case i.StreamOut:
|
||||||
req.SetStreamOut(&emptypb.Empty{})
|
req.SetStreamOut(&emptypb.Empty{})
|
||||||
return req
|
return req, nil
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPBResult(res []*pb.Response_Result) []stage.ExecutorResult {
|
func convertPBResult(res []*pb.Response_Result) []stage.ExecutorResult {
|
||||||
|
|
|
||||||
21
internal/executor/sandbox/convert_test.go
Normal file
21
internal/executor/sandbox/convert_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertPBCmdReturnsSourceReadError(t *testing.T) {
|
||||||
|
missing := t.TempDir() + "/missing"
|
||||||
|
_, err := convertPBCmd([]stage.Cmd{{
|
||||||
|
Args: []string{"true"},
|
||||||
|
CopyIn: map[string]stage.CmdFile{
|
||||||
|
"input": {Src: &missing},
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "read source file") {
|
||||||
|
t.Fatalf("convertPBCmd() error = %v, want source read error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"maps"
|
"maps"
|
||||||
|
|
@ -11,11 +12,11 @@ import (
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Sandbox) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
func (e *Sandbox) Run(ctx context.Context, cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
var err error
|
var err error
|
||||||
if e.execClient == nil {
|
if e.execClient == nil {
|
||||||
slog.Debug("create exec client", "server", e.execServer)
|
slog.Debug("create exec client", "server", e.execServer)
|
||||||
e.execClient, err = createExecClient(e.execServer, e.token)
|
e.execClient, e.conn, err = createExecClient(e.execServer, e.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -32,14 +33,19 @@ func (e *Sandbox) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pbCmds := convertPBCmd(cmds)
|
pbCmds, err := convertPBCmd(cmds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for i, pbCmd := range pbCmds {
|
for i, pbCmd := range pbCmds {
|
||||||
slog.Debug("sandbox execute", "i", i, "pbCmd size", proto.Size(pbCmd))
|
slog.Debug("sandbox execute", "i", i, "pbCmd size", proto.Size(pbCmd))
|
||||||
}
|
}
|
||||||
pbReq := &pb.Request{}
|
pbReq := &pb.Request{}
|
||||||
pbReq.SetCmd(pbCmds)
|
pbReq.SetCmd(pbCmds)
|
||||||
slog.Debug("sandbox execute", "pbReq size", proto.Size(pbReq))
|
slog.Debug("sandbox execute", "pbReq size", proto.Size(pbReq))
|
||||||
pbRet, err := e.execClient.Exec(context.TODO(), pbReq)
|
callCtx, cancel := context.WithTimeout(ctx, e.timeout)
|
||||||
|
defer cancel()
|
||||||
|
pbRet, err := e.execClient.Exec(callCtx, pbReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -53,15 +59,24 @@ func (e *Sandbox) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Sandbox) Cleanup() error {
|
func (e *Sandbox) Cleanup(ctx context.Context) error {
|
||||||
|
var cleanupErr error
|
||||||
for k, fileID := range e.cachedMap {
|
for k, fileID := range e.cachedMap {
|
||||||
req := &pb.FileID{}
|
req := &pb.FileID{}
|
||||||
req.SetFileID(fileID)
|
req.SetFileID(fileID)
|
||||||
_, err := e.execClient.FileDelete(context.TODO(), req)
|
callCtx, cancel := context.WithTimeout(ctx, e.timeout)
|
||||||
|
_, err := e.execClient.FileDelete(callCtx, req)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("sandbox cleanup", "error", err)
|
slog.Error("sandbox cleanup", "error", err)
|
||||||
|
cleanupErr = errors.Join(cleanupErr, err)
|
||||||
}
|
}
|
||||||
delete(e.cachedMap, k)
|
delete(e.cachedMap, k)
|
||||||
}
|
}
|
||||||
return nil
|
if e.conn != nil {
|
||||||
|
cleanupErr = errors.Join(cleanupErr, e.conn.Close())
|
||||||
|
e.conn = nil
|
||||||
|
e.execClient = nil
|
||||||
|
}
|
||||||
|
return cleanupErr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// copied from https://github.com/criyle/go-judger-demo/blob/master/apigateway/main.go
|
// copied from https://github.com/criyle/go-judger-demo/blob/master/apigateway/main.go
|
||||||
func createExecClient(execServer, token string) (pb.ExecutorClient, error) {
|
func createExecClient(execServer, token string) (pb.ExecutorClient, *grpc.ClientConn, error) {
|
||||||
conn, err := createGRPCConnection(execServer, token)
|
conn, err := createGRPCConnection(execServer, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("gRPC connection", "error", err)
|
slog.Error("gRPC connection", "error", err)
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return pb.NewExecutorClient(conn), nil
|
return pb.NewExecutorClient(conn), conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGRPCConnection(addr, token string) (*grpc.ClientConn, error) {
|
func createGRPCConnection(addr, token string) (*grpc.ClientConn, error) {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/criyle/go-judge/pb"
|
"github.com/criyle/go-judge/pb"
|
||||||
"github.com/joint-online-judge/JOJ3/internal/stage"
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var name = "sandbox"
|
var name = "sandbox"
|
||||||
|
|
@ -15,6 +18,8 @@ type Sandbox struct {
|
||||||
execServer, token string
|
execServer, token string
|
||||||
cachedMap map[string]string
|
cachedMap map[string]string
|
||||||
execClient pb.ExecutorClient
|
execClient pb.ExecutorClient
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -22,6 +27,7 @@ func init() {
|
||||||
execServer: "localhost:5051",
|
execServer: "localhost:5051",
|
||||||
token: "",
|
token: "",
|
||||||
cachedMap: make(map[string]string),
|
cachedMap: make(map[string]string),
|
||||||
|
timeout: 30 * time.Second,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,5 +37,6 @@ func InitWithConf(execServer, token string) {
|
||||||
execServer: execServer,
|
execServer: execServer,
|
||||||
token: token,
|
token: token,
|
||||||
cachedMap: make(map[string]string),
|
cachedMap: make(map[string]string),
|
||||||
|
timeout: 30 * time.Second,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package stage
|
package stage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -9,8 +10,8 @@ import (
|
||||||
var executorMap = map[string]Executor{}
|
var executorMap = map[string]Executor{}
|
||||||
|
|
||||||
type Executor interface {
|
type Executor interface {
|
||||||
Run([]Cmd) ([]ExecutorResult, error)
|
Run(context.Context, []Cmd) ([]ExecutorResult, error)
|
||||||
Cleanup() error
|
Cleanup(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterExecutor(name string, executor Executor) {
|
func RegisterExecutor(name string, executor Executor) {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@
|
||||||
package stage
|
package stage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(stages []Stage) (
|
func Run(ctx context.Context, stages []Stage) (
|
||||||
stageResults []StageResult, forceQuitStageName string, err error,
|
stageResults []StageResult, forceQuitStageName string, err error,
|
||||||
) {
|
) {
|
||||||
var executorResults []ExecutorResult
|
var executorResults []ExecutorResult
|
||||||
|
|
@ -59,9 +61,10 @@ func Run(stages []Stage) (
|
||||||
"name", stage.Executor.Name,
|
"name", stage.Executor.Name,
|
||||||
)
|
)
|
||||||
err = fmt.Errorf("executor not found: %s", stage.Executor.Name)
|
err = fmt.Errorf("executor not found: %s", stage.Executor.Name)
|
||||||
|
forceQuitStageName = stage.Name
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
executorResults, err = executor.Run(stage.Executor.Cmds)
|
executorResults, err = executor.Run(ctx, stage.Executor.Cmds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(
|
slog.Error(
|
||||||
"executor run error",
|
"executor run error",
|
||||||
|
|
@ -69,6 +72,7 @@ func Run(stages []Stage) (
|
||||||
"name", stage.Executor.Name,
|
"name", stage.Executor.Name,
|
||||||
"error", err,
|
"error", err,
|
||||||
)
|
)
|
||||||
|
forceQuitStageName = stage.Name
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, executorResult := range executorResults {
|
for i, executorResult := range executorResults {
|
||||||
|
|
@ -115,6 +119,7 @@ func Run(stages []Stage) (
|
||||||
"name", stageParser.Name,
|
"name", stageParser.Name,
|
||||||
)
|
)
|
||||||
err = fmt.Errorf("parser not found: %s", stageParser.Name)
|
err = fmt.Errorf("parser not found: %s", stageParser.Name)
|
||||||
|
forceQuitStageName = stage.Name
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var parserForceQuit bool
|
var parserForceQuit bool
|
||||||
|
|
@ -130,6 +135,14 @@ func Run(stages []Stage) (
|
||||||
forceQuitStageName = stage.Name
|
forceQuitStageName = stage.Name
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if len(tmpParserResults) != len(executorResults) {
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"parser %q returned %d results for %d executor results",
|
||||||
|
stageParser.Name, len(tmpParserResults), len(executorResults),
|
||||||
|
)
|
||||||
|
forceQuitStageName = stage.Name
|
||||||
|
break
|
||||||
|
}
|
||||||
for i, parserResult := range tmpParserResults {
|
for i, parserResult := range tmpParserResults {
|
||||||
parserScoresMap[stageParser.Name][i] += parserResult.Score
|
parserScoresMap[stageParser.Name][i] += parserResult.Score
|
||||||
}
|
}
|
||||||
|
|
@ -185,12 +198,15 @@ func Run(stages []Stage) (
|
||||||
return stageResults, forceQuitStageName, err
|
return stageResults, forceQuitStageName, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Cleanup() {
|
func Cleanup(ctx context.Context) error {
|
||||||
slog.Info("stage cleanup start")
|
slog.Info("stage cleanup start")
|
||||||
|
var cleanupErr error
|
||||||
for name, executor := range executorMap {
|
for name, executor := range executorMap {
|
||||||
err := executor.Cleanup()
|
err := executor.Cleanup(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("executor cleanup error", "name", name, "error", err)
|
slog.Error("executor cleanup error", "name", name, "error", err)
|
||||||
|
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("executor %q cleanup: %w", name, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cleanupErr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
internal/stage/run_test.go
Normal file
40
internal/stage/run_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package stage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contractTestExecutor struct{}
|
||||||
|
|
||||||
|
func (contractTestExecutor) Run(context.Context, []Cmd) ([]ExecutorResult, error) {
|
||||||
|
return []ExecutorResult{{Status: StatusAccepted}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (contractTestExecutor) Cleanup(context.Context) error { return nil }
|
||||||
|
|
||||||
|
type contractTestParser struct{}
|
||||||
|
|
||||||
|
func (contractTestParser) Run([]ExecutorResult, any) ([]ParserResult, bool, error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRejectsParserResultCountMismatch(t *testing.T) {
|
||||||
|
const executorName = "contract-test-executor"
|
||||||
|
const parserName = "contract-test-parser"
|
||||||
|
RegisterExecutor(executorName, contractTestExecutor{})
|
||||||
|
RegisterParser(parserName, contractTestParser{})
|
||||||
|
|
||||||
|
_, forceQuit, err := Run(context.Background(), []Stage{{
|
||||||
|
Name: "test",
|
||||||
|
Executor: StageExecutor{Name: executorName, Cmds: []Cmd{{}}},
|
||||||
|
Parsers: []StageParser{{Name: parserName}},
|
||||||
|
}})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "returned 0 results for 1") {
|
||||||
|
t.Fatalf("Run() error = %v, want result count error", err)
|
||||||
|
}
|
||||||
|
if forceQuit != "test" {
|
||||||
|
t.Fatalf("Run() force quit = %q, want test", forceQuit)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user