Skip to content

Commit

Permalink
add linter and flag for enabling resource constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
srujangit123 committed Dec 26, 2024
1 parent cb08ea4 commit ea5120f
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 58 deletions.
22 changes: 22 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
linters:
disable-all: true
enable:
- errcheck
- goconst
- gocyclo
- gofmt
- goimports
- gosec
- govet
- ineffassign
- lll
- misspell
- nakedret
- prealloc
- staticcheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
fast: false
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,22 @@ To start the server, run the following command:
```sh
./server
```
The default directory where the code files will be stored is `/tmp/`
A separate directory is created for every language.
To change the directory where the code files will be stored(eventually removed by the garbage collector), use

### Flags
- `--code-dir`
The default directory where the code files will be stored is `/tmp/`
A separate directory is created for every language.
To change the directory where the code files will be stored(eventually removed by the garbage collector), use
```sh
./server --code-dir /path/to/code/dir
```

- `--resource-constraints`
By default, resource constraints are turned off to improve the performance, if you want to enable it, use
```sh
./server --resource-constraints true
```

## API

### Submit Code
Expand Down
26 changes: 26 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"flag"
"remote-code-engine/pkg/config"

"go.uber.org/zap"
)

func ParseFlags() {
flag.StringVar(&config.BaseCodePath, "code-dir", "/tmp/", "Base path to store the code files")
flag.BoolVar(&config.ResourceConstraints, "resource-constraints", false, "Enable resource constraints (default false)")
help := flag.Bool("help", false, "Display help")

flag.Parse()

if *help {
flag.Usage()
return
}

logger.Info("parsed the flags",
zap.String("code-dir", config.BaseCodePath),
zap.Bool("resource-constraints", config.ResourceConstraints),
)
}
28 changes: 22 additions & 6 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"flag"
"net/http"
"os"
"remote-code-engine/pkg/config"
Expand Down Expand Up @@ -55,10 +54,11 @@ func setupCodeDirectory(imageConfig config.ImageConfig) {
}

func main() {
defer logger.Sync()
defer func() {
_ = logger.Sync()
}()

flag.StringVar(&config.BaseCodePath, "code-dir", "/tmp/", "Base path to store the code files")
flag.Parse()
ParseFlags()

// This should be given as a command line argument.
imageConfig, err := config.LoadConfig("../config.yml")
Expand All @@ -83,7 +83,23 @@ func main() {
panic(err)
}

go cli.FreeUpZombieContainers(context.Background())
ctx, cancel := context.WithCancel(context.Background())

StartServer(cli, imageConfig)
go func() {
err = cli.FreeUpZombieContainers(ctx)
if err != nil {
logger.Error("failed to free up zombie containers",
zap.Error(err),
)
}
}()

err = StartServer(cli, imageConfig)
if err != nil {
logger.Error("failed to start the server",
zap.Error(err),
)
cancel()
panic(err)
}
}
7 changes: 6 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const (
)

var (
BaseCodePath string
BaseCodePath string
ResourceConstraints bool
)

type LanguageConfig struct {
Expand Down Expand Up @@ -55,3 +56,7 @@ func (c *ImageConfig) IsLanguageSupported(lang Language) bool {
func GetHostLanguageCodePath(lang Language) string {
return filepath.Join(BaseCodePath, string(lang))
}

func IsResourceConstraintsEnabled() bool {
return ResourceConstraints
}
2 changes: 1 addition & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestLoadConfig(t *testing.T) {

// Write the sample config to a temporary file
configPath := filepath.Join(tempDir, "config.yaml")
err = os.WriteFile(configPath, data, 0644)
err = os.WriteFile(configPath, data, 0600)
if err != nil {
t.Fatalf("failed to write sample config to file: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/container/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "time"
const (
MAX_EXECUTION_TIME = 60 * time.Second

// The server running this will check for every 10 minutes whether there are zombie containers - completed containers and removes them.
// The server running this will check for every 10 minutes whether there are zombie containers.
GarbageCollectionTimeWindow = 5 * time.Minute

// Path where the code files are mounted.
Expand Down
108 changes: 65 additions & 43 deletions pkg/container/dockerclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewDockerClient(opts *client.Opt, logger *zap.Logger) (ContainerClient, err
}

func (d *dockerClient) GetContainers(ctx context.Context, opts *container.ListOptions) ([]Container, error) {
var containersList []Container
containersList := []Container{}

containers, err := d.client.ContainerList(ctx, *opts)
if err != nil {
Expand All @@ -66,6 +66,39 @@ func (d *dockerClient) GetContainers(ctx context.Context, opts *container.ListOp
return containersList, nil
}

func (d *dockerClient) getResourceConstraints() container.Resources {
if config.IsResourceConstraintsEnabled() {
return container.Resources{
Memory: 500 * 1024 * 1024, // 500 MB
NanoCPUs: 1000000000, // 1 CPU
Ulimits: []*units.Ulimit{
{
Name: "nproc",
Soft: 64,
Hard: 128,
},
{
Name: "nofile",
Soft: 64,
Hard: 128,
},
{
Name: "core",
Soft: 0,
Hard: 0,
},
{
// Maximum file size that can be created by the process (output file in our case)
Name: "fsize",
Soft: 20 * 1024 * 1024,
Hard: 20 * 1024 * 1024,
},
},
}
}
return container.Resources{}
}

func (d *dockerClient) ExecuteCode(ctx context.Context, code *Code) (string, error) {
codeFileName, inputFileName, err := createCodeAndInputFilesHost(code, d.logger)
if err != nil {
Expand All @@ -76,6 +109,8 @@ func (d *dockerClient) ExecuteCode(ctx context.Context, code *Code) (string, err
zap.String("input file name", inputFileName),
)

resourceConstraints := d.getResourceConstraints()

res, err := d.client.ContainerCreate(ctx, &container.Config{
Cmd: getContainerCommand(code, codeFileName, inputFileName),
Image: code.Image,
Expand All @@ -92,40 +127,13 @@ func (d *dockerClient) ExecuteCode(ctx context.Context, code *Code) (string, err
RestartPolicy: container.RestartPolicy{
Name: "no",
},
// We are reading the container logs to get the output. So it's better to disable and have a separate thread to delete stale containers
// We are reading the container logs to get the output.
// So it's better to disable and have a separate thread to delete stale containers
AutoRemove: false,
// Drop all the capabilities
CapDrop: []string{"ALL"},
Privileged: false,
// Set the memory limit to 1GB
Resources: container.Resources{
// set 500 MB as the memory limit in bytes
Memory: 500 * 1024 * 1024,
NanoCPUs: 500000000, // 0.5 CPU
Ulimits: []*units.Ulimit{
{
Name: "nproc",
Soft: 64,
Hard: 128,
},
{
Name: "nofile",
Soft: 64,
Hard: 128,
},
{
Name: "core",
Soft: 0,
Hard: 0,
},
{
// Maximum file size that can be created by the process (output file in our case)
Name: "fsize",
Soft: 20 * 1024 * 1024,
Hard: 20 * 1024 * 1024,
},
},
},
Resources: resourceConstraints,
}, nil, nil, getContainerName())

if err != nil {
Expand Down Expand Up @@ -217,22 +225,36 @@ func deleteStaleFiles(dir string, logger *zap.Logger) error {
}

func (d *dockerClient) FreeUpZombieContainers(ctx context.Context) error {
ticker := time.NewTicker(GarbageCollectionTimeWindow)
for {
pruneResults, err := d.client.ContainersPrune(ctx, filters.Args{})
if err != nil {
d.logger.Error("failed to prune containers",
zap.Error(err),
)
}
select {
case <-ctx.Done():
d.logger.Info("stopping the zombie container cleanup routine")
return nil
case <-ticker.C:
pruneResults, err := d.client.ContainersPrune(ctx, filters.Args{})
if err != nil {
d.logger.Error("failed to prune containers",
zap.Error(err),
)
}

d.logger.Info("successfully pruned the containers:",
zap.Int("#Pruned containers", len(pruneResults.ContainersDeleted)),
)
d.logger.Info("successfully pruned the containers:",
zap.Int("#Pruned containers", len(pruneResults.ContainersDeleted)),
)

deleteStaleFiles(config.GetHostLanguageCodePath(config.Cpp), d.logger)
deleteStaleFiles(config.GetHostLanguageCodePath(config.Golang), d.logger)
if err = deleteStaleFiles(config.GetHostLanguageCodePath(config.Cpp), d.logger); err != nil {
d.logger.Error("failed to delete stale files",
zap.Error(err),
)
}

time.Sleep(GarbageCollectionTimeWindow)
if err = deleteStaleFiles(config.GetHostLanguageCodePath(config.Golang), d.logger); err != nil {
d.logger.Error("failed to delete stale files",
zap.Error(err),
)
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/container/file-helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func createFile(filePath, base64FileContent string, logger *zap.Logger) (string,
return filepath.Base(filePath), fmt.Errorf("failed to decode the file content: %w", err)
}

n, err := f.Write([]byte(data))
n, err := f.Write(data)
if err != nil {
return filepath.Base(filePath), fmt.Errorf("failed to write the content to the file: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/container/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ type Code struct {

type ContainerClient interface {
FreeUpZombieContainers(ctx context.Context) error
ExecuteCode(ctx context.Context, code *Code) (string, error) // Executes and returns the output in the string, error in case of server errors not code errors.

// Executes and returns the output in the string, error in case of server errors not code errors.
ExecuteCode(ctx context.Context, code *Code) (string, error)

// TODO: Is this even needed?
GetContainers(ctx context.Context, opts *container.ListOptions) ([]Container, error) // Remove list options if you want some other container type other than docker
// Remove list options if you want some other container type other than docker
GetContainers(ctx context.Context, opts *container.ListOptions) ([]Container, error)
}

0 comments on commit ea5120f

Please sign in to comment.