Skip to content
Merged
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
106 changes: 99 additions & 7 deletions api/internal/activity/tracker.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
// Package activity provides session activity tracking and idle detection for StreamSpace.
//
// The activity tracker monitors user interaction with sessions and implements
// idle timeout-based auto-hibernation. Unlike the connection tracker which
// monitors network connections, this tracker monitors actual user activity
// (keyboard, mouse, application interaction).
//
// Features:
// - LastActivity timestamp tracking in Kubernetes Session status
// - Idle duration calculation based on lastActivity
// - Configurable idle timeouts per session (spec.idleTimeout)
// - Auto-hibernation after idle threshold + grace period
// - Background idle session monitor
//
// Architecture:
// - Stateless (reads from Kubernetes directly)
// - Updates Session.status.lastActivity via Kubernetes API
// - Runs periodic checks for idle sessions
// - Hibernates sessions by updating state to "hibernated"
//
// Hibernation triggers:
// - User interaction stopped for > idleTimeout
// - Grace period of 5 minutes after threshold
// - Only applies to sessions with idleTimeout configured
// - Only hibernates sessions in "running" state
//
// Example usage:
//
// tracker := activity.NewTracker(k8sClient)
//
// // Update activity on user interaction
// tracker.UpdateSessionActivity(ctx, "streamspace", "user1-firefox")
//
// // Start background idle monitor
// go tracker.StartIdleMonitor(ctx, "streamspace", 1*time.Minute)
package activity

import (
Expand All @@ -9,25 +44,82 @@ import (
"github.com/streamspace/streamspace/api/internal/k8s"
)

// Tracker manages session activity tracking
// Tracker manages session activity tracking for idle detection and auto-hibernation.
//
// This tracker is stateless and reads directly from Kubernetes Session resources.
// It updates the status.lastActivity field and monitors for idle sessions.
//
// Difference from connection tracker:
// - Connection tracker: Monitors network connections (WebSocket, VNC)
// - Activity tracker: Monitors user interaction (keyboard, mouse, app activity)
//
// A session can have active connections but be idle (user not interacting),
// or vice versa (background processes running, no active user).
//
// Example:
//
// tracker := NewTracker(k8sClient)
// err := tracker.UpdateSessionActivity(ctx, namespace, sessionName)
type Tracker struct {
// k8sClient interacts with Kubernetes to read and update Sessions.
k8sClient *k8s.Client
}

// NewTracker creates a new activity tracker
// NewTracker creates a new activity tracker instance.
//
// The tracker is stateless and can be shared across goroutines.
//
// Example:
//
// tracker := NewTracker(k8sClient)
// go tracker.StartIdleMonitor(ctx, "streamspace", 1*time.Minute)
func NewTracker(k8sClient *k8s.Client) *Tracker {
return &Tracker{
k8sClient: k8sClient,
}
}

// ActivityStatus represents the activity state of a session
// ActivityStatus represents the current activity state of a session.
//
// This status is calculated from:
// - status.lastActivity: Last user interaction timestamp
// - spec.idleTimeout: Configured idle timeout (e.g., "30m")
// - Current time: Compared against lastActivity
//
// States:
// - IsActive: User has interacted recently (within idle threshold)
// - IsIdle: No interaction for longer than idle threshold
// - ShouldHibernate: Idle + grace period elapsed (ready for hibernation)
//
// Example:
//
// status := tracker.GetActivityStatus(session)
// if status.ShouldHibernate {
// log.Printf("Session has been idle for %v", status.IdleDuration)
// }
type ActivityStatus struct {
IsActive bool
IsIdle bool
LastActivity *time.Time
IdleDuration time.Duration
// IsActive indicates if the session has recent activity.
// True if lastActivity is within idleThreshold.
IsActive bool

// IsIdle indicates if the session has exceeded the idle threshold.
// True if lastActivity is older than idleThreshold.
IsIdle bool

// LastActivity is the timestamp of the last user interaction.
// Nil if no activity has been recorded yet (newly created session).
LastActivity *time.Time

// IdleDuration is how long the session has been idle.
// Calculated as time.Since(lastActivity).
IdleDuration time.Duration

// IdleThreshold is the configured timeout from spec.idleTimeout.
// Example: 30m, 1h, 2h30m
IdleThreshold time.Duration

// ShouldHibernate indicates if the session should be auto-hibernated.
// True if idle for > threshold + 5 minute grace period.
ShouldHibernate bool
}

Expand Down
81 changes: 76 additions & 5 deletions api/internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,87 @@
// Package errors provides standardized error handling for StreamSpace API.
//
// This package implements a consistent error format across all API endpoints:
// - Structured error responses with error codes
// - Automatic HTTP status code mapping
// - Optional error details for debugging
// - Machine-readable error codes for client error handling
//
// Error Structure:
// - Code: Machine-readable error identifier (e.g., "QUOTA_EXCEEDED")
// - Message: Human-readable error message
// - Details: Optional additional context (wrapped errors, stack traces)
// - StatusCode: HTTP status code (400, 401, 403, 404, 500, etc.)
//
// Error Categories:
// - Client Errors (4xx): Bad request, unauthorized, forbidden, not found
// - Server Errors (5xx): Internal errors, database errors, service unavailable
//
// Usage patterns:
//
// // Simple error
// return errors.NotFound("session")
//
// // Error with custom message
// return errors.QuotaExceeded("Maximum 5 sessions allowed")
//
// // Wrap underlying error
// return errors.DatabaseError(err)
//
// // In HTTP handler
// c.JSON(err.StatusCode, err.ToResponse())
//
// JSON Response Format:
//
// {
// "error": "QUOTA_EXCEEDED",
// "message": "Session quota exceeded",
// "code": "QUOTA_EXCEEDED",
// "details": "5/5 sessions active"
// }
package errors

import (
"fmt"
"net/http"
)

// AppError represents a standardized application error
// AppError represents a standardized application error with HTTP context.
//
// AppError provides:
// - Machine-readable error code for client error handling
// - Human-readable message for display to users
// - Optional details for debugging (not always shown to clients)
// - Automatic HTTP status code mapping
//
// Example:
//
// err := &AppError{
// Code: "QUOTA_EXCEEDED",
// Message: "Session quota exceeded: 5/5 sessions active",
// Details: "user1 has 5 running sessions, max allowed is 5",
// StatusCode: 403,
// }
type AppError struct {
Code string `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
StatusCode int `json:"-"`
// Code is a machine-readable error identifier.
// Format: UPPER_SNAKE_CASE (e.g., "QUOTA_EXCEEDED", "NOT_FOUND")
// Used by clients for programmatic error handling.
Code string `json:"code"`

// Message is a human-readable error description.
// Should be suitable for display to end users.
// Example: "Session quota exceeded: 5/5 sessions active"
Message string `json:"message"`

// Details provides additional context for debugging (optional).
// May contain wrapped error messages, stack traces, or technical details.
// Should not be shown to end users in production.
// Example: "database query failed: connection timeout"
Details string `json:"details,omitempty"`

// StatusCode is the HTTP status code to return.
// Automatically set based on error code.
// Not included in JSON response (marked with `json:"-"`)
StatusCode int `json:"-"`
}

// Error implements the error interface
Expand Down
Loading
Loading