Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 51 additions & 50 deletions apps/runner/cmd/runner/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,57 @@ import (
)

type Config struct {
DaytonaApiUrl string `envconfig:"DAYTONA_API_URL"`
ApiToken string `envconfig:"DAYTONA_RUNNER_TOKEN"`
ApiPort int `envconfig:"API_PORT"`
ApiLogRequests bool `envconfig:"API_LOG_REQUESTS" default:"false"`
TLSCertFile string `envconfig:"TLS_CERT_FILE"`
TLSKeyFile string `envconfig:"TLS_KEY_FILE"`
EnableTLS bool `envconfig:"ENABLE_TLS"`
OtelLoggingEnabled bool `envconfig:"OTEL_LOGGING_ENABLED"`
OtelTracingEnabled bool `envconfig:"OTEL_TRACING_ENABLED"`
OtelEndpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT"`
OtelHeaders string `envconfig:"OTEL_EXPORTER_OTLP_HEADERS"`
BackupInfoCacheRetention time.Duration `envconfig:"BACKUP_INFO_CACHE_RETENTION" default:"168h" validate:"min=5m"`
Environment string `envconfig:"ENVIRONMENT"`
ContainerRuntime string `envconfig:"CONTAINER_RUNTIME"`
ContainerNetwork string `envconfig:"CONTAINER_NETWORK"`
InterSandboxNetworkEnabled bool `envconfig:"INTER_SANDBOX_NETWORK_ENABLED" default:"true"`
GpuEnabled bool `envconfig:"GPU_ENABLED" default:"false"`
LogFilePath string `envconfig:"LOG_FILE_PATH"`
AWSRegion string `envconfig:"AWS_REGION"`
AWSEndpointUrl string `envconfig:"AWS_ENDPOINT_URL"`
AWSAccessKeyId string `envconfig:"AWS_ACCESS_KEY_ID"`
AWSSecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY"`
AWSDefaultBucket string `envconfig:"AWS_DEFAULT_BUCKET"`
ResourceLimitsDisabled bool `envconfig:"RESOURCE_LIMITS_DISABLED"`
DaemonStartTimeoutSec int `envconfig:"DAEMON_START_TIMEOUT_SEC"`
SandboxStartTimeoutSec int `envconfig:"SANDBOX_START_TIMEOUT_SEC"`
AndroidBootTimeoutSec int `envconfig:"ANDROID_BOOT_TIMEOUT_SEC"`
UseSnapshotEntrypoint bool `envconfig:"USE_SNAPSHOT_ENTRYPOINT"`
Domain string `envconfig:"RUNNER_DOMAIN" validate:"omitempty,hostname|ip"`
VolumeCleanupInterval time.Duration `envconfig:"VOLUME_CLEANUP_INTERVAL" default:"30s" validate:"min=10s"`
VolumeCleanupDryRun bool `envconfig:"VOLUME_CLEANUP_DRY_RUN" default:"true"`
VolumeCleanupExclusionPeriod time.Duration `envconfig:"VOLUME_CLEANUP_EXCLUSION_PERIOD" default:"120s" validate:"min=0s"`
PollTimeout time.Duration `envconfig:"POLL_TIMEOUT" default:"30s"`
PollLimit int `envconfig:"POLL_LIMIT" default:"10" validate:"min=1,max=100"`
CollectorWindowSize int `envconfig:"COLLECTOR_WINDOW_SIZE" default:"60" validate:"min=1"`
CPUUsageSnapshotInterval time.Duration `envconfig:"CPU_USAGE_SNAPSHOT_INTERVAL" default:"5s" validate:"min=1s"`
AllocatedResourcesSnapshotInterval time.Duration `envconfig:"ALLOCATED_RESOURCES_SNAPSHOT_INTERVAL" default:"5s" validate:"min=1s"`
HealthcheckInterval time.Duration `envconfig:"HEALTHCHECK_INTERVAL" default:"30s" validate:"min=10s"`
HealthcheckTimeout time.Duration `envconfig:"HEALTHCHECK_TIMEOUT" default:"10s"`
BackupTimeoutMin int `envconfig:"BACKUP_TIMEOUT_MIN" default:"60" validate:"min=1"`
SnapshotPullTimeout time.Duration `envconfig:"SNAPSHOT_PULL_TIMEOUT" default:"60m" validate:"min=1m"`
BuildTimeoutMin int `envconfig:"BUILD_TIMEOUT_MIN" default:"120" validate:"min=1"`
BuildCPUCores int64 `envconfig:"BUILD_CPU_CORES" default:"4" validate:"min=1"`
BuildMemoryGB int64 `envconfig:"BUILD_MEMORY_GB" default:"8" validate:"min=1"`
ApiVersion int `envconfig:"API_VERSION" default:"2"`
InitializeDaemonTelemetry bool `envconfig:"INITIALIZE_DAEMON_TELEMETRY" default:"true"`
SnapshotErrorCacheRetention time.Duration `envconfig:"SNAPSHOT_ERROR_CACHE_RETENTION" default:"10m" validate:"min=5m"`
BuildEngine string `envconfig:"BUILD_ENGINE" default:"buildkit" validate:"oneof=buildkit legacy"`
ForceSnapshotRemoval bool `envconfig:"FORCE_SNAPSHOT_REMOVAL" default:"true"`
MountKvmToAndroidSandbox bool `envconfig:"MOUNT_KVM_TO_ANDROID_SANDBOX" default:"false"`
DaytonaApiUrl string `envconfig:"DAYTONA_API_URL"`
ApiToken string `envconfig:"DAYTONA_RUNNER_TOKEN"`
ApiPort int `envconfig:"API_PORT"`
ApiLogRequests bool `envconfig:"API_LOG_REQUESTS" default:"false"`
TLSCertFile string `envconfig:"TLS_CERT_FILE"`
TLSKeyFile string `envconfig:"TLS_KEY_FILE"`
EnableTLS bool `envconfig:"ENABLE_TLS"`
OtelLoggingEnabled bool `envconfig:"OTEL_LOGGING_ENABLED"`
OtelTracingEnabled bool `envconfig:"OTEL_TRACING_ENABLED"`
OtelEndpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT"`
OtelHeaders string `envconfig:"OTEL_EXPORTER_OTLP_HEADERS"`
BackupInfoCacheRetention time.Duration `envconfig:"BACKUP_INFO_CACHE_RETENTION" default:"168h" validate:"min=5m"`
Environment string `envconfig:"ENVIRONMENT"`
ContainerRuntime string `envconfig:"CONTAINER_RUNTIME"`
ContainerNetwork string `envconfig:"CONTAINER_NETWORK"`
InterSandboxNetworkEnabled bool `envconfig:"INTER_SANDBOX_NETWORK_ENABLED" default:"true"`
GpuEnabled bool `envconfig:"GPU_ENABLED" default:"false"`
LogFilePath string `envconfig:"LOG_FILE_PATH"`
AWSRegion string `envconfig:"AWS_REGION"`
AWSEndpointUrl string `envconfig:"AWS_ENDPOINT_URL"`
AWSAccessKeyId string `envconfig:"AWS_ACCESS_KEY_ID"`
AWSSecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY"`
AWSDefaultBucket string `envconfig:"AWS_DEFAULT_BUCKET"`
ResourceLimitsDisabled bool `envconfig:"RESOURCE_LIMITS_DISABLED"`
DaemonStartTimeoutSec int `envconfig:"DAEMON_START_TIMEOUT_SEC"`
SandboxStartTimeoutSec int `envconfig:"SANDBOX_START_TIMEOUT_SEC"`
AndroidBootTimeoutSec int `envconfig:"ANDROID_BOOT_TIMEOUT_SEC"`
UseSnapshotEntrypoint bool `envconfig:"USE_SNAPSHOT_ENTRYPOINT"`
Domain string `envconfig:"RUNNER_DOMAIN" validate:"omitempty,hostname|ip"`
VolumeCleanupInterval time.Duration `envconfig:"VOLUME_CLEANUP_INTERVAL" default:"30s" validate:"min=10s"`
VolumeCleanupDryRun bool `envconfig:"VOLUME_CLEANUP_DRY_RUN" default:"true"`
VolumeCleanupExclusionPeriod time.Duration `envconfig:"VOLUME_CLEANUP_EXCLUSION_PERIOD" default:"120s" validate:"min=0s"`
PollTimeout time.Duration `envconfig:"POLL_TIMEOUT" default:"30s"`
PollLimit int `envconfig:"POLL_LIMIT" default:"10" validate:"min=1,max=100"`
CollectorWindowSize int `envconfig:"COLLECTOR_WINDOW_SIZE" default:"60" validate:"min=1"`
CPUUsageSnapshotInterval time.Duration `envconfig:"CPU_USAGE_SNAPSHOT_INTERVAL" default:"5s" validate:"min=1s"`
AllocatedResourcesSnapshotInterval time.Duration `envconfig:"ALLOCATED_RESOURCES_SNAPSHOT_INTERVAL" default:"5s" validate:"min=1s"`
SandboxFdUsageWarningThresholdPercent int `envconfig:"SANDBOX_FD_USAGE_WARNING_THRESHOLD_PERCENT" default:"70" validate:"min=1,max=100"`
HealthcheckInterval time.Duration `envconfig:"HEALTHCHECK_INTERVAL" default:"30s" validate:"min=10s"`
HealthcheckTimeout time.Duration `envconfig:"HEALTHCHECK_TIMEOUT" default:"10s"`
BackupTimeoutMin int `envconfig:"BACKUP_TIMEOUT_MIN" default:"60" validate:"min=1"`
SnapshotPullTimeout time.Duration `envconfig:"SNAPSHOT_PULL_TIMEOUT" default:"60m" validate:"min=1m"`
BuildTimeoutMin int `envconfig:"BUILD_TIMEOUT_MIN" default:"120" validate:"min=1"`
BuildCPUCores int64 `envconfig:"BUILD_CPU_CORES" default:"4" validate:"min=1"`
BuildMemoryGB int64 `envconfig:"BUILD_MEMORY_GB" default:"8" validate:"min=1"`
ApiVersion int `envconfig:"API_VERSION" default:"2"`
InitializeDaemonTelemetry bool `envconfig:"INITIALIZE_DAEMON_TELEMETRY" default:"true"`
SnapshotErrorCacheRetention time.Duration `envconfig:"SNAPSHOT_ERROR_CACHE_RETENTION" default:"10m" validate:"min=5m"`
BuildEngine string `envconfig:"BUILD_ENGINE" default:"buildkit" validate:"oneof=buildkit legacy"`
ForceSnapshotRemoval bool `envconfig:"FORCE_SNAPSHOT_REMOVAL" default:"true"`
MountKvmToAndroidSandbox bool `envconfig:"MOUNT_KVM_TO_ANDROID_SANDBOX" default:"false"`
}

var DEFAULT_API_PORT int = 8080
Expand Down
11 changes: 6 additions & 5 deletions apps/runner/cmd/runner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,12 @@ func run() int {

// Create metrics collector
metricsCollector := metrics.NewCollector(metrics.CollectorConfig{
Logger: logger,
Docker: dockerClient,
WindowSize: cfg.CollectorWindowSize,
CPUUsageSnapshotInterval: cfg.CPUUsageSnapshotInterval,
AllocatedResourcesSnapshotInterval: cfg.AllocatedResourcesSnapshotInterval,
Logger: logger,
Docker: dockerClient,
WindowSize: cfg.CollectorWindowSize,
CPUUsageSnapshotInterval: cfg.CPUUsageSnapshotInterval,
AllocatedResourcesSnapshotInterval: cfg.AllocatedResourcesSnapshotInterval,
SandboxFdUsageWarningThresholdPercent: float64(cfg.SandboxFdUsageWarningThresholdPercent),
})
metricsCollector.Start(ctx)

Expand Down
2 changes: 1 addition & 1 deletion apps/runner/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/opencontainers/image-spec v1.1.1
github.com/orcaman/concurrent-map/v2 v2.0.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/procfs v0.20.1
github.com/samber/slog-gin v1.20.1
github.com/shirou/gopsutil/v4 v4.25.12
github.com/swaggo/files v1.0.1
Expand Down Expand Up @@ -120,7 +121,6 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
Expand Down
122 changes: 116 additions & 6 deletions apps/runner/internal/metrics/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"errors"
"fmt"
"log/slog"
"runtime"
"strings"
"sync"
"time"

Expand All @@ -30,6 +32,9 @@ type CollectorConfig struct {
WindowSize int
CPUUsageSnapshotInterval time.Duration
AllocatedResourcesSnapshotInterval time.Duration
// SandboxFdUsageWarningThresholdPercent is the worst-process fd usage
// percentage at or above which a sandbox is warned about.
SandboxFdUsageWarningThresholdPercent float64
}

// Collector collects system metrics
Expand All @@ -47,6 +52,11 @@ type Collector struct {
allocatedDiskGiB float32
startedSandboxCount float32

// Per-sandbox host-side fd usage sampling (Linux only)
fdSamplingEnabled bool
lastFdSandboxIds map[string]struct{}
fdWarn *fdWarnTracker

// Intervals for snapshotting metrics in seconds
cpuUsageSnapshotInterval time.Duration
allocatedResourcesSnapshotInterval time.Duration
Expand Down Expand Up @@ -82,11 +92,18 @@ func NewCollector(cfg CollectorConfig) *Collector {
cpuRing: ring.New(cfg.WindowSize),
cpuUsageSnapshotInterval: cfg.CPUUsageSnapshotInterval,
allocatedResourcesSnapshotInterval: cfg.AllocatedResourcesSnapshotInterval,
fdSamplingEnabled: runtime.GOOS == "linux",
lastFdSandboxIds: make(map[string]struct{}),
fdWarn: newFdWarnTracker(cfg.SandboxFdUsageWarningThresholdPercent),
}
}

// Start begins needed metrics collection processes
func (c *Collector) Start(ctx context.Context) {
if !c.fdSamplingEnabled {
c.log.InfoContext(ctx, "Per-sandbox file descriptor sampling disabled", "goos", runtime.GOOS)
}

go c.snapshotCPUUsage(ctx)
go c.snapshotAllocatedResources(ctx)
}
Expand Down Expand Up @@ -254,8 +271,19 @@ func (c *Collector) snapshotAllocatedResources(ctx context.Context) {
var totalAllocatedDiskGB float32 = 0 // Disk in GB
var startedSandboxCount float32 = 0 // Count of running containers

var seenFdSandboxIds map[string]struct{}
if c.fdSamplingEnabled {
seenFdSandboxIds = make(map[string]struct{})
}

for _, ctr := range containers {
cpu, memory, disk, err := c.getContainerAllocatedResources(ctx, ctr.ID)
containerJSON, err := c.docker.ContainerInspect(ctx, ctr.ID)
if err != nil {
c.log.WarnContext(ctx, "Failed to get allocated resources for container", "container_id", ctr.ID, "error", err)
continue
}

cpu, memory, disk, err := getContainerAllocatedResources(containerJSON)
if err != nil {
c.log.WarnContext(ctx, "Failed to get allocated resources for container", "container_id", ctr.ID, "error", err)
continue
Expand All @@ -270,6 +298,16 @@ func (c *Collector) snapshotAllocatedResources(ctx context.Context) {

// For disk: count all containers (running and stopped)
totalAllocatedDiskGB += disk

if c.fdSamplingEnabled && ctr.State == "running" && containerJSON.State != nil && containerJSON.State.Pid > 0 {
c.sampleContainerFdUsage(ctx, containerJSON, seenFdSandboxIds)
}
}

if c.fdSamplingEnabled {
c.cleanupFdMetrics(seenFdSandboxIds)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
c.fdWarn.prune(seenFdSandboxIds)
c.lastFdSandboxIds = seenFdSandboxIds
}

// Convert to original API units
Expand All @@ -283,13 +321,85 @@ func (c *Collector) snapshotAllocatedResources(ctx context.Context) {
}
}

func (c *Collector) getContainerAllocatedResources(ctx context.Context, containerId string) (float32, float32, float32, error) {
// Inspect the container to get its resource configuration
containerJSON, err := c.docker.ContainerInspect(ctx, containerId)
// sampleContainerFdUsage samples host-side fd usage of a running sandbox
// container and updates the per-sandbox Prometheus gauges. The container name
// is the sandbox ID.
func (c *Collector) sampleContainerFdUsage(ctx context.Context, containerJSON *container.InspectResponse, seen map[string]struct{}) {
sandboxId := strings.TrimPrefix(containerJSON.Name, "/")

// The sandbox is present whether or not sampling succeeds below:
// recording it in seen up front keeps the warn tracker's rate-limit
// state across transient sampling failures, so recovery does not
// immediately re-warn.
seen[sandboxId] = struct{}{}

usage, err := sampleSandboxFdUsage(procRoot, cgroupRoot, containerJSON.State.Pid)
if err != nil {
return 0, 0, 0, err
c.log.DebugContext(ctx, "Failed to sample sandbox fd usage", "sandbox_id", sandboxId, "error", err)
// cleanupFdMetrics skips seen sandboxes, so drop this sandbox's
// gauges explicitly: no stale values for the failed tick.
deleteSandboxFdMetrics(sandboxId)
return
}

common.SandboxOpenFds.WithLabelValues(sandboxId).Set(float64(usage.OpenFds))
Comment on lines +328 to +345

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2df2ffe: presence is recorded before sampling, so a transient sampling failure no longer prunes warn state; gauges are dropped for the failed tick only.

common.SandboxFdLimit.WithLabelValues(sandboxId).Set(float64(usage.WorstFdLimit))
common.SandboxFdUsagePercent.WithLabelValues(sandboxId).Set(usage.UsagePercent)

switch c.fdWarn.observe(sandboxId, usage.UsagePercent, time.Now()) {
case fdWarnEventWarn:
c.log.WarnContext(ctx, "Sandbox file descriptor usage above threshold",
"sandbox_id", sandboxId,
"open_fds", usage.OpenFds,
"worst_pid", usage.WorstPid,
"worst_open_fds", usage.WorstOpenFds,
"fd_limit", usage.WorstFdLimit,
"usage_percent", usage.UsagePercent,
"threshold_percent", c.fdWarn.thresholdPercent)
case fdWarnEventClear:
c.log.InfoContext(ctx, "Sandbox file descriptor usage recovered",
"sandbox_id", sandboxId,
"usage_percent", usage.UsagePercent,
"threshold_percent", c.fdWarn.thresholdPercent)
case fdWarnEventNone:
}

// Best-effort fd attribution for runtime helper processes (shim and
// friends) whenever a runtime name is present in HostConfig. dockerd
// backfills the default runtime name ("runc") at create, so this also
// runs for default-runtime containers; attribution is best-effort for
// any runtime. Never feeds the warning/percentage path; on failure the
// metric is simply absent.
if containerJSON.HostConfig != nil && containerJSON.HostConfig.Runtime != "" {
helperFds, err := sampleRuntimeHelperFds(procRoot, containerJSON.State.Pid, usage.memberPids)
if err != nil {
c.log.DebugContext(ctx, "Failed to sample runtime helper fd usage", "sandbox_id", sandboxId, "error", err)
common.SandboxRuntimeHelperOpenFds.DeleteLabelValues(sandboxId)
} else {
common.SandboxRuntimeHelperOpenFds.WithLabelValues(sandboxId).Set(float64(helperFds))
}
}
}

// cleanupFdMetrics deletes gauge label sets for sandboxes that disappeared
// since the previous snapshot so no stale series linger.
func (c *Collector) cleanupFdMetrics(seen map[string]struct{}) {
for sandboxId := range c.lastFdSandboxIds {
if _, ok := seen[sandboxId]; !ok {
deleteSandboxFdMetrics(sandboxId)
}
}
}

// deleteSandboxFdMetrics drops every fd gauge label set of a sandbox.
func deleteSandboxFdMetrics(sandboxId string) {
common.SandboxOpenFds.DeleteLabelValues(sandboxId)
common.SandboxFdLimit.DeleteLabelValues(sandboxId)
common.SandboxFdUsagePercent.DeleteLabelValues(sandboxId)
common.SandboxRuntimeHelperOpenFds.DeleteLabelValues(sandboxId)
}

func getContainerAllocatedResources(containerJSON *container.InspectResponse) (float32, float32, float32, error) {
if containerJSON.HostConfig == nil {
return 0, 0, 0, nil
}
Expand All @@ -313,7 +423,7 @@ func (c *Collector) getContainerAllocatedResources(ctx context.Context, containe
// Disk allocation from StorageOpt (assuming xfs filesystem)
storageGB, err := common.ParseStorageOptSizeGB(containerJSON.HostConfig.StorageOpt)
if err != nil {
return allocatedCpu, allocatedMemory, 0, fmt.Errorf("error parsing storage quota for container %s: %v", containerId, err)
return allocatedCpu, allocatedMemory, 0, fmt.Errorf("error parsing storage quota for container %s: %v", containerJSON.ID, err)
}

if storageGB > 0 {
Expand Down
Loading