-
Notifications
You must be signed in to change notification settings - Fork 148
sandboxset: thread-safe sandbox pool (issue #217)
#425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4d351bc
47d5d10
8037432
a357a25
e7fda1f
13ba7a0
f85697d
ae2ac69
34638f4
83e27f4
01ae0cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 } | ||
| func (m *MockSandbox) Meta() *SandboxMeta { return nil } | ||
| func (m *MockSandbox) GetRuntimeLog() string { return "" } | ||
| func (m *MockSandbox) GetProxyLog() string { return "" } | ||
| 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 | ||
| } | ||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the API, they don't need to know about internals.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No error, should always succeed (or crash). |
||
| return newSandboxSet(cfg) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.