feat(sandbox): enhance lifecycle management and box status handling
- Updated BuildIdentifier to include assistant ID in session identifiers for better uniqueness. - Implemented automatic starting of stopped boxes in resolveBox, improving recovery processes. - Introduced status management for boxes, allowing for accurate tracking of their state (running, exited, stopped). - Added idle timeout defaults based on lifecycle policy in BuildCreateOptions, enhancing configuration flexibility. - Refactored tests to validate new box status behavior and lifecycle management improvements. Made-with: Cursor
This commit is contained in:
parent
6d5ae17d2f
commit
3797091d37
7 changed files with 219 additions and 120 deletions
|
|
@ -22,16 +22,9 @@ func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID, wor
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom identifier from metadata takes precedence.
|
|
||||||
if metadata != nil {
|
|
||||||
if cid, ok := metadata["computer_id"].(string); ok && cid != "" {
|
|
||||||
return fmt.Sprintf("%s-%s.%s", ownerID, cid, workspaceID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cfg.Lifecycle {
|
switch cfg.Lifecycle {
|
||||||
case "session":
|
case "session":
|
||||||
return fmt.Sprintf("%s-%s", ownerID, chatID)
|
return fmt.Sprintf("%s-%s-%s", ownerID, assistantID, chatID)
|
||||||
case "longrunning", "persistent":
|
case "longrunning", "persistent":
|
||||||
return fmt.Sprintf("%s-%s.%s", ownerID, assistantID, workspaceID)
|
return fmt.Sprintf("%s-%s.%s", ownerID, assistantID, workspaceID)
|
||||||
default:
|
default:
|
||||||
|
|
@ -184,8 +177,17 @@ func resolveBox(
|
||||||
if identifier != "" {
|
if identifier != "" {
|
||||||
box, err := manager.Get(context.Background(), identifier)
|
box, err := manager.Get(context.Background(), identifier)
|
||||||
if err == nil && box != nil {
|
if err == nil && box != nil {
|
||||||
box.BindWorkplace(workspaceID)
|
if box.IsStopped() {
|
||||||
return box, identifier, nil
|
if startErr := manager.StartBox(context.Background(), identifier); startErr != nil {
|
||||||
|
log.Printf("[sandbox/v2] auto-start stopped box %s failed: %v, creating new", identifier, startErr)
|
||||||
|
} else {
|
||||||
|
box.BindWorkplace(workspaceID)
|
||||||
|
return box, identifier, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
box.BindWorkplace(workspaceID)
|
||||||
|
return box, identifier, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,14 @@ func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspace
|
||||||
}
|
}
|
||||||
opts.IdleTimeout = d
|
opts.IdleTimeout = d
|
||||||
}
|
}
|
||||||
|
if opts.IdleTimeout == 0 {
|
||||||
|
switch opts.Policy {
|
||||||
|
case infra.Session:
|
||||||
|
opts.IdleTimeout = infra.DefaultSessionIdleTimeout
|
||||||
|
case infra.LongRunning:
|
||||||
|
opts.IdleTimeout = infra.DefaultLongRunningIdleTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
if cfg.MaxLifetime != "" {
|
if cfg.MaxLifetime != "" {
|
||||||
d, err := time.ParseDuration(cfg.MaxLifetime)
|
d, err := time.ParseDuration(cfg.MaxLifetime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type Box struct {
|
||||||
lastCall atomic.Int64
|
lastCall atomic.Int64
|
||||||
lastHeartbeat atomic.Int64
|
lastHeartbeat atomic.Int64
|
||||||
processCount atomic.Int32
|
processCount atomic.Int32
|
||||||
|
status atomic.Value // string: "running", "exited", "stopped", "created", "unknown"
|
||||||
idleTimeoutD time.Duration
|
idleTimeoutD time.Duration
|
||||||
maxLifetimeD time.Duration
|
maxLifetimeD time.Duration
|
||||||
stopTimeoutD time.Duration
|
stopTimeoutD time.Duration
|
||||||
|
|
@ -210,14 +211,18 @@ func (b *Box) GetWorkDir() string {
|
||||||
func (b *Box) WorkspaceID() string { return b.workspaceID }
|
func (b *Box) WorkspaceID() string { return b.workspaceID }
|
||||||
|
|
||||||
// Snapshot returns a local-only BoxInfo snapshot without any remote calls.
|
// Snapshot returns a local-only BoxInfo snapshot without any remote calls.
|
||||||
// Status is inferred from local state (not from the container runtime).
|
// Status is maintained by the sandbox watcher (see watcher.go).
|
||||||
func (b *Box) Snapshot() BoxInfo {
|
func (b *Box) Snapshot() BoxInfo {
|
||||||
|
s, _ := b.status.Load().(string)
|
||||||
|
if s == "" {
|
||||||
|
s = "unknown"
|
||||||
|
}
|
||||||
return BoxInfo{
|
return BoxInfo{
|
||||||
ID: b.id,
|
ID: b.id,
|
||||||
ContainerID: b.containerID,
|
ContainerID: b.containerID,
|
||||||
NodeID: b.nodeID,
|
NodeID: b.nodeID,
|
||||||
Owner: b.owner,
|
Owner: b.owner,
|
||||||
Status: "running",
|
Status: s,
|
||||||
Policy: b.policy,
|
Policy: b.policy,
|
||||||
Labels: b.labels,
|
Labels: b.labels,
|
||||||
Image: b.image,
|
Image: b.image,
|
||||||
|
|
@ -313,6 +318,12 @@ func (b *Box) lastActiveTime() time.Time {
|
||||||
return time.UnixMilli(ts)
|
return time.UnixMilli(ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// idleSince returns the timestamp of the last business call (Exec/Stream/VNC/etc).
|
||||||
|
// Unlike lastActiveTime, heartbeats do NOT reset this — only real user activity does.
|
||||||
|
func (b *Box) idleSince() time.Time {
|
||||||
|
return time.UnixMilli(b.lastCall.Load())
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Box) idleTimeout() time.Duration {
|
func (b *Box) idleTimeout() time.Duration {
|
||||||
return b.idleTimeoutD
|
return b.idleTimeoutD
|
||||||
}
|
}
|
||||||
|
|
@ -327,3 +338,22 @@ func (b *Box) stopTimeout() time.Duration {
|
||||||
}
|
}
|
||||||
return DefaultStopTimeout
|
return DefaultStopTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStopped reports whether the box's last known status indicates a non-running container.
|
||||||
|
func (b *Box) IsStopped() bool {
|
||||||
|
s, _ := b.status.Load().(string)
|
||||||
|
return s == "exited" || s == "stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspectStatus queries the container runtime for the real container state.
|
||||||
|
func (b *Box) inspectStatus(ctx context.Context) string {
|
||||||
|
res, err := b.manager.getNode(b.nodeID)
|
||||||
|
if err != nil || res.Runtime == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
info, err := res.Runtime.Inspect(ctx, b.containerID)
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return info.Status
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager manages sandbox lifecycle. Node connections are delegated to tai/registry.
|
// Manager manages sandbox lifecycle. Node connections are delegated to tai/registry.
|
||||||
|
// Idle-timeout and lifecycle enforcement is handled by the sandbox watcher (watcher.go).
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
boxes sync.Map
|
boxes sync.Map
|
||||||
mu sync.Mutex
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newManager() *Manager {
|
func newManager() *Manager {
|
||||||
return &Manager{}
|
return &Manager{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start discovers existing containers from all registered nodes, rebuilds
|
// Start discovers existing containers from all registered nodes and rebuilds
|
||||||
// the boxes map, and starts the cleanup loop.
|
// the boxes map. If no "local" node is registered yet, it probes the local
|
||||||
// If no "local" node is registered yet, it probes the local Docker environment
|
// Docker environment and auto-registers one when available.
|
||||||
// and auto-registers one when available.
|
|
||||||
func (m *Manager) Start(ctx context.Context) error {
|
func (m *Manager) Start(ctx context.Context) error {
|
||||||
reg := registry.Global()
|
reg := registry.Global()
|
||||||
if reg == nil {
|
if reg == nil {
|
||||||
|
|
@ -49,9 +47,6 @@ func (m *Manager) Start(ctx context.Context) error {
|
||||||
m.recoverBoxes(ctx, snap.TaiID, res)
|
m.recoverBoxes(ctx, snap.TaiID, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
loopCtx, cancel := context.WithCancel(ctx)
|
|
||||||
m.cancel = cancel
|
|
||||||
go m.cleanupLoop(loopCtx)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +213,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
|
||||||
displayName: opts.DisplayName,
|
displayName: opts.DisplayName,
|
||||||
system: sys,
|
system: sys,
|
||||||
}
|
}
|
||||||
|
box.status.Store("running")
|
||||||
box.lastCall.Store(time.Now().UnixMilli())
|
box.lastCall.Store(time.Now().UnixMilli())
|
||||||
|
|
||||||
m.boxes.Store(id, box)
|
m.boxes.Store(id, box)
|
||||||
|
|
@ -267,6 +263,31 @@ func (m *Manager) List(_ context.Context, opts ListOptions) ([]*Box, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartBox starts a stopped sandbox and updates its lastCall timestamp.
|
||||||
|
func (m *Manager) StartBox(ctx context.Context, id string) error {
|
||||||
|
v, ok := m.boxes.Load(id)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
b := v.(*Box)
|
||||||
|
|
||||||
|
res, err := m.getNode(b.nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.Runtime == nil {
|
||||||
|
return fmt.Errorf("sandbox: node %q has no container runtime", b.nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := res.Runtime.Start(ctx, b.containerID); err != nil {
|
||||||
|
return fmt.Errorf("sandbox: start container %s: %w", b.containerID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.status.Store("running")
|
||||||
|
b.touch()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Remove force-removes a sandbox (SIGKILL + delete).
|
// Remove force-removes a sandbox (SIGKILL + delete).
|
||||||
func (m *Manager) Remove(ctx context.Context, id string) error {
|
func (m *Manager) Remove(ctx context.Context, id string) error {
|
||||||
v, ok := m.boxes.Load(id)
|
v, ok := m.boxes.Load(id)
|
||||||
|
|
@ -284,58 +305,11 @@ func (m *Manager) Remove(ctx context.Context, id string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup removes idle/expired sandboxes.
|
// Close is a no-op; lifecycle management is handled by the sandbox watcher.
|
||||||
func (m *Manager) Cleanup(ctx context.Context) error {
|
|
||||||
now := time.Now()
|
|
||||||
m.boxes.Range(func(key, value any) bool {
|
|
||||||
b := value.(*Box)
|
|
||||||
idle := now.Sub(b.lastActiveTime())
|
|
||||||
|
|
||||||
switch b.policy {
|
|
||||||
case OneShot:
|
|
||||||
// handled after Exec
|
|
||||||
case Session:
|
|
||||||
if timeout := b.idleTimeout(); timeout > 0 && idle > timeout {
|
|
||||||
m.Remove(ctx, b.id)
|
|
||||||
}
|
|
||||||
case LongRunning:
|
|
||||||
if timeout := b.idleTimeout(); timeout > 0 && idle > timeout {
|
|
||||||
if res, err := m.getNode(b.nodeID); err == nil && res.Runtime != nil {
|
|
||||||
res.Runtime.Stop(ctx, b.containerID, b.stopTimeout())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lifetime := b.maxLifetime(); lifetime > 0 && now.Sub(b.createdAt) > lifetime {
|
|
||||||
m.Remove(ctx, b.id)
|
|
||||||
}
|
|
||||||
case Persistent:
|
|
||||||
// never auto-cleaned
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close stops the cleanup loop. Node connections are managed by the registry.
|
|
||||||
func (m *Manager) Close() error {
|
func (m *Manager) Close() error {
|
||||||
if m.cancel != nil {
|
|
||||||
m.cancel()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) cleanupLoop(ctx context.Context) {
|
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
m.Cleanup(ctx)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) getNode(name string) (*tai.ConnResources, error) {
|
func (m *Manager) getNode(name string) (*tai.ConnResources, error) {
|
||||||
res, ok := tai.GetResources(name)
|
res, ok := tai.GetResources(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -488,12 +462,13 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn
|
||||||
if sys.OS == "" {
|
if sys.OS == "" {
|
||||||
sys = inferSystemInfo(ctx, res, c.Image)
|
sys = inferSystemInfo(ctx, res, c.Image)
|
||||||
}
|
}
|
||||||
|
policy := LifecyclePolicy(c.Labels["sandbox-policy"])
|
||||||
box := &Box{
|
box := &Box{
|
||||||
id: sandboxID,
|
id: sandboxID,
|
||||||
containerID: cid,
|
containerID: cid,
|
||||||
nodeID: c.Labels["sandbox-node-id"],
|
nodeID: c.Labels["sandbox-node-id"],
|
||||||
owner: c.Labels["sandbox-owner"],
|
owner: c.Labels["sandbox-owner"],
|
||||||
policy: LifecyclePolicy(c.Labels["sandbox-policy"]),
|
policy: policy,
|
||||||
labels: c.Labels,
|
labels: c.Labels,
|
||||||
createdAt: time.Now(),
|
createdAt: time.Now(),
|
||||||
image: c.Image,
|
image: c.Image,
|
||||||
|
|
@ -504,6 +479,12 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn
|
||||||
system: sys,
|
system: sys,
|
||||||
manager: m,
|
manager: m,
|
||||||
}
|
}
|
||||||
|
switch policy {
|
||||||
|
case Session:
|
||||||
|
box.idleTimeoutD = DefaultSessionIdleTimeout
|
||||||
|
case LongRunning:
|
||||||
|
box.idleTimeoutD = DefaultLongRunningIdleTimeout
|
||||||
|
}
|
||||||
box.lastCall.Store(time.Now().UnixMilli())
|
box.lastCall.Store(time.Now().UnixMilli())
|
||||||
m.boxes.Store(sandboxID, box)
|
m.boxes.Store(sandboxID, box)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,42 +46,6 @@ func TestHeartbeatUnknownBox(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIdleCleanup(t *testing.T) {
|
|
||||||
skipIfNoDocker(t)
|
|
||||||
|
|
||||||
for _, pc := range testNodes() {
|
|
||||||
pc := pc
|
|
||||||
t.Run(pc.Name, func(t *testing.T) {
|
|
||||||
m := setupManagerForNode(t, &pc)
|
|
||||||
ensureTestImage(t, m, pc.TaiID)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
box, err := m.Create(ctx, sandbox.CreateOptions{
|
|
||||||
Image: testImage(),
|
|
||||||
Owner: "test-user",
|
|
||||||
NodeID: pc.TaiID,
|
|
||||||
Policy: sandbox.Session,
|
|
||||||
IdleTimeout: 1 * time.Second,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Create: %v", err)
|
|
||||||
}
|
|
||||||
boxID := box.ID()
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
if err := m.Cleanup(ctx); err != nil {
|
|
||||||
t.Fatalf("Cleanup: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = m.Get(ctx, boxID)
|
|
||||||
if err != sandbox.ErrNotFound {
|
|
||||||
t.Errorf("after idle cleanup, Get err = %v, want ErrNotFound", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStartRecovery(t *testing.T) {
|
func TestStartRecovery(t *testing.T) {
|
||||||
skipIfNoDocker(t)
|
skipIfNoDocker(t)
|
||||||
|
|
||||||
|
|
@ -114,27 +78,50 @@ func TestStartRecovery(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPersistentNotCleaned(t *testing.T) {
|
func TestStartBox(t *testing.T) {
|
||||||
skipIfNoDocker(t)
|
skipIfNoDocker(t)
|
||||||
|
|
||||||
for _, pc := range testNodes() {
|
for _, pc := range testNodes() {
|
||||||
pc := pc
|
pc := pc
|
||||||
t.Run(pc.Name, func(t *testing.T) {
|
t.Run(pc.Name, func(t *testing.T) {
|
||||||
m := setupManagerForNode(t, &pc)
|
m := setupManagerForNode(t, &pc)
|
||||||
|
box := createTestBox(t, m, pc)
|
||||||
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
|
boxID := box.ID()
|
||||||
co.Policy = sandbox.Persistent
|
|
||||||
co.IdleTimeout = 1 * time.Second
|
|
||||||
})
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
m.Cleanup(ctx)
|
|
||||||
|
|
||||||
_, err := m.Get(ctx, box.ID())
|
if err := box.Stop(ctx); err != nil {
|
||||||
|
t.Fatalf("Stop: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
if err := m.StartBox(ctx, boxID); err != nil {
|
||||||
|
t.Fatalf("StartBox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := box.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("persistent box should not be cleaned: %v", err)
|
t.Fatalf("Info after StartBox: %v", err)
|
||||||
|
}
|
||||||
|
if info.Status != "running" {
|
||||||
|
t.Errorf("status = %q after StartBox, want running", info.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotReadsStatus(t *testing.T) {
|
||||||
|
skipIfNoDocker(t)
|
||||||
|
|
||||||
|
for _, pc := range testNodes() {
|
||||||
|
pc := pc
|
||||||
|
t.Run(pc.Name, func(t *testing.T) {
|
||||||
|
m := setupManagerForNode(t, &pc)
|
||||||
|
box := createTestBox(t, m, pc)
|
||||||
|
|
||||||
|
snap := box.Snapshot()
|
||||||
|
if snap.Status != "running" {
|
||||||
|
t.Errorf("initial snapshot status = %q, want running", snap.Status)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,11 @@ const (
|
||||||
Persistent LifecyclePolicy = "persistent"
|
Persistent LifecyclePolicy = "persistent"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultStopTimeout = 2 * time.Second
|
const (
|
||||||
|
DefaultStopTimeout = 2 * time.Second
|
||||||
|
DefaultSessionIdleTimeout = 30 * time.Minute
|
||||||
|
DefaultLongRunningIdleTimeout = 2 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Create / List options
|
// Create / List options
|
||||||
|
|
|
||||||
87
sandbox/v2/watcher.go
Normal file
87
sandbox/v2/watcher.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/yao/monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
monitor.Register(&sandboxWatcher{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type sandboxWatcher struct{}
|
||||||
|
|
||||||
|
func (w *sandboxWatcher) Name() string { return "sandbox" }
|
||||||
|
func (w *sandboxWatcher) Interval() time.Duration { return 30 * time.Second }
|
||||||
|
|
||||||
|
func (w *sandboxWatcher) Check(ctx context.Context) []monitor.Alert {
|
||||||
|
if mgr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var alerts []monitor.Alert
|
||||||
|
mgr.boxes.Range(func(_, v any) bool {
|
||||||
|
b := v.(*Box)
|
||||||
|
|
||||||
|
status := b.inspectStatus(ctx)
|
||||||
|
old, _ := b.status.Swap(status).(string)
|
||||||
|
if old != "" && old != status {
|
||||||
|
alerts = append(alerts, monitor.Alert{
|
||||||
|
Level: monitor.Info,
|
||||||
|
Target: "box:" + b.id,
|
||||||
|
Message: fmt.Sprintf("status %s → %s", old, status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != "running" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
idle := time.Since(b.idleSince())
|
||||||
|
timeout := b.idleTimeout()
|
||||||
|
if timeout <= 0 || idle <= timeout {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch b.policy {
|
||||||
|
case Session:
|
||||||
|
alerts = append(alerts, monitor.Alert{
|
||||||
|
Level: monitor.Warn,
|
||||||
|
Target: "box:" + b.id,
|
||||||
|
Message: fmt.Sprintf("session idle expired (idle=%s, timeout=%s), removing", idle.Round(time.Second), timeout),
|
||||||
|
Action: func(ctx context.Context) {
|
||||||
|
mgr.Remove(ctx, b.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case LongRunning:
|
||||||
|
alerts = append(alerts, monitor.Alert{
|
||||||
|
Level: monitor.Warn,
|
||||||
|
Target: "box:" + b.id,
|
||||||
|
Message: fmt.Sprintf("longrunning idle expired (idle=%s, timeout=%s), stopping", idle.Round(time.Second), timeout),
|
||||||
|
Action: func(ctx context.Context) {
|
||||||
|
b.Stop(ctx)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.policy == LongRunning {
|
||||||
|
if lifetime := b.maxLifetime(); lifetime > 0 && time.Since(b.createdAt) > lifetime {
|
||||||
|
alerts = append(alerts, monitor.Alert{
|
||||||
|
Level: monitor.Warn,
|
||||||
|
Target: "box:" + b.id,
|
||||||
|
Message: fmt.Sprintf("lifetime expired (%s), removing", lifetime),
|
||||||
|
Action: func(ctx context.Context) {
|
||||||
|
mgr.Remove(ctx, b.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return alerts
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue