e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
193 lines
4.9 KiB
Go
193 lines
4.9 KiB
Go
// Copyright 2026 Alibaba Group Holding Ltd.
|
|
//
|
|
// 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 isolation
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// UpperManager manages upper directories for overlay workspaces.
|
|
type UpperManager struct {
|
|
root string
|
|
maxBytes int64
|
|
mu sync.Mutex
|
|
entries map[string]*UpperEntry
|
|
}
|
|
|
|
// UpperEntry tracks one allocated upper directory.
|
|
type UpperEntry struct {
|
|
UpperDir string
|
|
WorkDir string
|
|
InUse bool
|
|
}
|
|
|
|
// NewUpperManager creates an upper directory manager.
|
|
func NewUpperManager(root string, maxBytes int64) (*UpperManager, error) {
|
|
if root == "" {
|
|
return nil, errors.New("upper: root path is required")
|
|
}
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
return nil, fmt.Errorf("upper: create root %s: %w", root, err)
|
|
}
|
|
return &UpperManager{
|
|
root: root,
|
|
maxBytes: maxBytes,
|
|
entries: make(map[string]*UpperEntry),
|
|
}, nil
|
|
}
|
|
|
|
// ErrUpperLimitExceeded is returned when the upper directory size limit is exceeded.
|
|
var ErrUpperLimitExceeded = errors.New("upper: total usage exceeds configured limit")
|
|
|
|
// Allocate creates a new upper + work directory pair. Returns the session ID
|
|
// and the directories. Returns ErrUpperLimitExceeded if maxBytes > 0 and
|
|
// current usage already meets or exceeds the limit.
|
|
func (m *UpperManager) Allocate() (sessionID, upperDir, workDir string, err error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.maxBytes > 0 {
|
|
usage, usageErr := m.usageLocked()
|
|
if usageErr == nil && usage >= m.maxBytes {
|
|
return "", "", "", fmt.Errorf("%w: %d >= %d bytes", ErrUpperLimitExceeded, usage, m.maxBytes)
|
|
}
|
|
}
|
|
|
|
id := newSessionID()
|
|
upperDir = filepath.Join(m.root, id, "upper")
|
|
workDir = filepath.Join(m.root, id, "work")
|
|
|
|
if err := os.MkdirAll(upperDir, 0o755); err != nil {
|
|
return "", "", "", fmt.Errorf("upper: mkdir %s: %w", upperDir, err)
|
|
}
|
|
if err := os.MkdirAll(workDir, 0o755); err != nil {
|
|
os.RemoveAll(filepath.Dir(upperDir))
|
|
return "", "", "", fmt.Errorf("upper: mkdir %s: %w", workDir, err)
|
|
}
|
|
|
|
m.entries[id] = &UpperEntry{
|
|
UpperDir: upperDir,
|
|
WorkDir: workDir,
|
|
InUse: true,
|
|
}
|
|
|
|
return id, upperDir, workDir, nil
|
|
}
|
|
|
|
// Release marks an upper directory as available for GC.
|
|
func (m *UpperManager) Release(sessionID string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if e, ok := m.entries[sessionID]; ok {
|
|
e.InUse = false
|
|
}
|
|
}
|
|
|
|
// Remove immediately deletes an upper directory.
|
|
func (m *UpperManager) Remove(sessionID string) error {
|
|
m.mu.Lock()
|
|
e, ok := m.entries[sessionID]
|
|
if !ok {
|
|
m.mu.Unlock()
|
|
return fmt.Errorf("upper: session %s not found", sessionID)
|
|
}
|
|
delete(m.entries, sessionID)
|
|
m.mu.Unlock()
|
|
|
|
upperParent := filepath.Dir(e.UpperDir)
|
|
return os.RemoveAll(upperParent)
|
|
}
|
|
|
|
// Collect runs one garbage collection pass, removing all released entries.
|
|
func (m *UpperManager) Collect() []string {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
var freed []string
|
|
for id, e := range m.entries {
|
|
if !e.InUse {
|
|
upperParent := filepath.Dir(e.UpperDir)
|
|
if err := os.RemoveAll(upperParent); err == nil {
|
|
freed = append(freed, id)
|
|
delete(m.entries, id)
|
|
}
|
|
}
|
|
}
|
|
return freed
|
|
}
|
|
|
|
// Usage returns the current total size of all upper directories in bytes.
|
|
func (m *UpperManager) Usage() (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.usageLocked()
|
|
}
|
|
|
|
// usageLocked calculates usage without acquiring the mutex. Caller must hold m.mu.
|
|
func (m *UpperManager) usageLocked() (int64, error) {
|
|
var total int64
|
|
for _, e := range m.entries {
|
|
size, err := dirSize(e.UpperDir)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
total += size
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// Root returns the manager's root path.
|
|
func (m *UpperManager) Root() string {
|
|
return m.root
|
|
}
|
|
|
|
// MaxBytes returns the configured byte limit.
|
|
func (m *UpperManager) MaxBytes() int64 {
|
|
return m.maxBytes
|
|
}
|
|
|
|
// newSessionID generates a random hex session ID.
|
|
func newSessionID() string {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
// Cryptographic randomness shouldn't fail. Fall back to a
|
|
// timestamp-based name as last resort.
|
|
return fmt.Sprintf("fallback-%d", os.Getpid())
|
|
}
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// dirSize walks a directory and returns total bytes used.
|
|
func dirSize(path string) (int64, error) {
|
|
var size int64
|
|
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
size += info.Size()
|
|
}
|
|
return nil
|
|
})
|
|
return size, err
|
|
}
|