Skip to content
Open
2 changes: 2 additions & 0 deletions go/common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ func checkConf(cfg *Config) error {
if cfg.Features.Import_cache != "" {
return fmt.Errorf("features.import_cache must be disabled for docker Sandbox")
}
} else if cfg.Sandbox == "mock" {
// mock sandbox: no additional requirements
} else {
return fmt.Errorf("Unknown Sandbox type '%s'", cfg.Sandbox)
}
Expand Down
122 changes: 122 additions & 0 deletions go/worker/sandbox/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package sandbox

import (
"fmt"
"net/http"
"sync"
"sync/atomic"
)

// MockSandbox is a test double for Sandbox.
// Exported fields control error injection; state fields track lifecycle.
type MockSandbox struct {
mu sync.Mutex
id string
paused bool
destroyed bool

// Set these before calling Get/Put to inject errors.
PauseErr error
UnpauseErr error
}

var mockIDCounter int64

// NewMockSandbox creates a MockSandbox with the given ID.
func NewMockSandbox(id string) *MockSandbox {
return &MockSandbox{id: id, paused: true}
}

func (m *MockSandbox) ID() string { return m.id }

func (m *MockSandbox) Destroy(reason string) {
m.mu.Lock()
defer m.mu.Unlock()
m.destroyed = true
}

func (m *MockSandbox) DestroyIfPaused(reason string) {
m.mu.Lock()
defer m.mu.Unlock()
if m.paused {
m.destroyed = true
}
}

func (m *MockSandbox) Pause() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.PauseErr != nil {
return m.PauseErr
}
m.paused = true
return nil
}

func (m *MockSandbox) Unpause() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.UnpauseErr != nil {
return m.UnpauseErr
}
m.paused = false
return nil
}

func (m *MockSandbox) Client() *http.Client { return nil }

Check failure on line 66 in go/worker/sandbox/mock.go

View workflow job for this annotation

GitHub Actions / continuous-integration

method receiver 'm' is not referenced in method's body, consider removing or renaming it as _
func (m *MockSandbox) Meta() *SandboxMeta { return nil }

Check failure on line 67 in go/worker/sandbox/mock.go

View workflow job for this annotation

GitHub Actions / continuous-integration

method receiver 'm' is not referenced in method's body, consider removing or renaming it as _
func (m *MockSandbox) GetRuntimeLog() string { return "" }

Check failure on line 68 in go/worker/sandbox/mock.go

View workflow job for this annotation

GitHub Actions / continuous-integration

method receiver 'm' is not referenced in method's body, consider removing or renaming it as _
func (m *MockSandbox) GetProxyLog() string { return "" }

Check failure on line 69 in go/worker/sandbox/mock.go

View workflow job for this annotation

GitHub Actions / continuous-integration

method receiver 'm' is not referenced in method's body, consider removing or renaming it as _
func (m *MockSandbox) DebugString() string { return fmt.Sprintf("mock:%s", m.id) }
func (m *MockSandbox) fork(dst Sandbox) error { return nil }
func (m *MockSandbox) childExit(child Sandbox) {}

// IsDestroyed returns whether Destroy has been called.
func (m *MockSandbox) IsDestroyed() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.destroyed
}

// IsPaused returns the current pause state.
func (m *MockSandbox) IsPaused() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.paused
}

// MockSandboxPool is a test double for SandboxPool.
// It creates MockSandbox instances with auto-incremented IDs.
type MockSandboxPool struct {
mu sync.Mutex
Created []*MockSandbox

// Set before calling Get to make pool.Create fail.
CreateErr error
}

func (p *MockSandboxPool) Create(parent Sandbox, isLeaf bool, codeDir, scratchDir string, meta *SandboxMeta) (Sandbox, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.CreateErr != nil {
return nil, p.CreateErr
}
id := fmt.Sprintf("mock-%d", atomic.AddInt64(&mockIDCounter, 1))
sb := NewMockSandbox(id)
sb.paused = false // Pool.Create returns unpaused sandboxes
p.Created = append(p.Created, sb)
return sb, nil
}

func (p *MockSandboxPool) Cleanup() {}
func (p *MockSandboxPool) AddListener(handler SandboxEventFunc) {}
func (p *MockSandboxPool) DebugString() string { return "mock-pool" }

// CreatedSandboxes returns a snapshot of all sandboxes created by this pool.
func (p *MockSandboxPool) CreatedSandboxes() []*MockSandbox {
p.mu.Lock()
defer p.mu.Unlock()
out := make([]*MockSandbox, len(p.Created))
copy(out, p.Created)
return out
}
2 changes: 2 additions & 0 deletions go/worker/sandbox/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func SandboxPoolFromConfig(name string, sizeMb int) (cf SandboxPool, err error)
}
NewSOCKEvictor(pool)
return pool, nil
} else if common.Conf.Sandbox == "mock" {
return &MockSandboxPool{}, nil
}

return nil, fmt.Errorf("invalid sandbox type: '%s'", common.Conf.Sandbox)
Expand Down
79 changes: 79 additions & 0 deletions go/worker/sandboxset/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Package sandboxset provides a thread-safe pool of sandboxes for a single Lambda function. Callers ask for a sandbox and don't worry about whether it is freshly created or recycled from a previous request.
//
// Sandbox lifecycle inside a SandboxSet:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the API, they don't need to know about internals.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage is good because that's what API users need. The internals in the flow diagram are not something users of your code should worry about. Those are internal details.

//
// [created]
// |
// v
// [paused] <---+
// | |
// v |
// [in-use] ----+ (Put)
// |
// v
// [destroyed] (Destroy / Close / error)
//
// Usage:
//
// set, err := sandboxset.New(&sandboxset.Config{
// Pool: myPool,
// CodeDir: "/path/to/lambda",
// ScratchDirs: myScratchDirs,
// })
//
// ref, err := set.GetOrCreateUnpaused()
// // ... use ref.Sandbox() to handle request ...
// if broken {
// ref.Broken = true
// }
// ref.Put()
package sandboxset

import (
"github.com/open-lambda/open-lambda/go/common"
"github.com/open-lambda/open-lambda/go/worker/sandbox"
)

// SandboxSet manages a pool of sandboxes for one Lambda function.
// All methods are safe to call from multiple goroutines.
type SandboxSet interface {
// GetOrCreateUnpaused returns an unpaused sandbox ready to handle a
// request, wrapped in a SandboxRef.
GetOrCreateUnpaused() (*SandboxRef, error)

// Close destroys all sandboxes in the pool and marks the set as closed.
Close() error
}

// Config holds the parameters needed to create a SandboxSet.
type Config struct {
// Pool creates and destroys the underlying sandboxes.
Pool sandbox.SandboxPool

// Parent is an optional SandboxSet to fork from. When nil, new
// sandboxes are created from scratch. Not all SandboxPool
// implementations support forking.
Parent SandboxSet

// IsLeaf marks sandboxes as non-forkable, meaning they will
// not be used as parents for future forks.
IsLeaf bool

// CodeDir is the directory containing the Lambda handler code.
CodeDir string

// Meta holds runtime configuration (memory limits, packages,
// imports, etc.). Nil means the pool fills in defaults.
Meta *sandbox.SandboxMeta

// ScratchDirs creates a unique writable directory for each
// new sandbox. The set calls ScratchDirs.Make internally
// so that GetOrCreateUnpaused can remain argument-free.
ScratchDirs *common.DirMaker
}

// New creates a SandboxSet from cfg. Returns an error if any of
// Pool, CodeDir, or ScratchDirs are missing.
func New(cfg *Config) (SandboxSet, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No error, should always succeed (or crash).

return newSandboxSet(cfg)
}
Loading
Loading