Update API layer for scheduler integration: - WebSocket handlers with scheduler protocol support - Jobs WebSocket endpoint with priority queue integration - Validation middleware for scheduler messages - Server configuration with security hardening - Protocol definitions for worker-scheduler communication - Dataset handlers with tenant isolation checks - Response helpers with audit context - OpenAPI spec updates for new endpoints
185 lines
5.5 KiB
Go
185 lines
5.5 KiB
Go
// Package helpers provides shared utilities for WebSocket handlers.
|
|
package helpers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/queue"
|
|
)
|
|
|
|
// ErrorCode represents WebSocket error codes
|
|
type ErrorCode byte
|
|
|
|
// TaskErrorMapper maps task errors to error codes
|
|
type TaskErrorMapper struct{}
|
|
|
|
// NewTaskErrorMapper creates a new task error mapper
|
|
func NewTaskErrorMapper() *TaskErrorMapper {
|
|
return &TaskErrorMapper{}
|
|
}
|
|
|
|
// MapError maps a task error to an error code based on status and error message
|
|
func (m *TaskErrorMapper) MapError(t *queue.Task, defaultCode ErrorCode) ErrorCode {
|
|
if t == nil {
|
|
return defaultCode
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(t.Status))
|
|
errStr := strings.ToLower(strings.TrimSpace(t.Error))
|
|
|
|
if status == "cancelled" {
|
|
return 0x24 // ErrorCodeJobCancelled
|
|
}
|
|
if strings.Contains(errStr, "out of memory") || strings.Contains(errStr, "oom") {
|
|
return 0x30 // ErrorCodeOutOfMemory
|
|
}
|
|
if strings.Contains(errStr, "no space left") || strings.Contains(errStr, "disk full") {
|
|
return 0x31 // ErrorCodeDiskFull
|
|
}
|
|
if strings.Contains(errStr, "rate limit") || strings.Contains(errStr, "too many requests") || strings.Contains(errStr, "throttle") {
|
|
return 0x33 // ErrorCodeServiceUnavailable
|
|
}
|
|
if strings.Contains(errStr, "timed out") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "deadline") {
|
|
return 0x14 // ErrorCodeTimeout
|
|
}
|
|
if strings.Contains(errStr, "connection refused") || strings.Contains(errStr, "connection reset") || strings.Contains(errStr, "network unreachable") {
|
|
return 0x12 // ErrorCodeNetworkError
|
|
}
|
|
if strings.Contains(errStr, "queue") && strings.Contains(errStr, "not configured") {
|
|
return 0x32 // ErrorCodeInvalidConfiguration
|
|
}
|
|
|
|
// Default for worker-side execution failures
|
|
if status == "failed" {
|
|
return 0x23 // ErrorCodeJobExecutionFailed
|
|
}
|
|
return defaultCode
|
|
}
|
|
|
|
// MapJupyterError maps Jupyter task errors to error codes
|
|
func (m *TaskErrorMapper) MapJupyterError(t *queue.Task) ErrorCode {
|
|
if t == nil {
|
|
return 0x00 // ErrorCodeUnknownError
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(t.Status))
|
|
errStr := strings.ToLower(strings.TrimSpace(t.Error))
|
|
|
|
if status == "cancelled" {
|
|
return 0x24 // ErrorCodeJobCancelled
|
|
}
|
|
if strings.Contains(errStr, "out of memory") || strings.Contains(errStr, "oom") {
|
|
return 0x30 // ErrorCodeOutOfMemory
|
|
}
|
|
if strings.Contains(errStr, "no space left") || strings.Contains(errStr, "disk full") {
|
|
return 0x31 // ErrorCodeDiskFull
|
|
}
|
|
if strings.Contains(errStr, "rate limit") || strings.Contains(errStr, "too many requests") || strings.Contains(errStr, "throttle") {
|
|
return 0x33 // ErrorCodeServiceUnavailable
|
|
}
|
|
if strings.Contains(errStr, "timed out") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "deadline") {
|
|
return 0x14 // ErrorCodeTimeout
|
|
}
|
|
if strings.Contains(errStr, "connection refused") || strings.Contains(errStr, "connection reset") || strings.Contains(errStr, "network unreachable") {
|
|
return 0x12 // ErrorCodeNetworkError
|
|
}
|
|
if strings.Contains(errStr, "queue") && strings.Contains(errStr, "not configured") {
|
|
return 0x32 // ErrorCodeInvalidConfiguration
|
|
}
|
|
|
|
// Default for worker-side execution failures
|
|
if status == "failed" {
|
|
return 0x23 // ErrorCodeJobExecutionFailed
|
|
}
|
|
return 0x00 // ErrorCodeUnknownError
|
|
}
|
|
|
|
// ResourceRequest represents resource requirements
|
|
type ResourceRequest struct {
|
|
GPUMemory string
|
|
CPU int
|
|
MemoryGB int
|
|
GPU int
|
|
}
|
|
|
|
// ParseResourceRequest parses an optional resource request from bytes.
|
|
// Format: [cpu:1][memory_gb:1][gpu:1][gpu_mem_len:1][gpu_mem:var]
|
|
// If payload is empty, returns nil.
|
|
func ParseResourceRequest(payload []byte) (*ResourceRequest, error) {
|
|
if len(payload) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(payload) < 4 {
|
|
return nil, fmt.Errorf("resource payload too short")
|
|
}
|
|
cpu := int(payload[0])
|
|
mem := int(payload[1])
|
|
gpu := int(payload[2])
|
|
gpuMemLen := int(payload[3])
|
|
if gpuMemLen < 0 || len(payload) < 4+gpuMemLen {
|
|
return nil, fmt.Errorf("invalid gpu memory length")
|
|
}
|
|
gpuMem := ""
|
|
if gpuMemLen > 0 {
|
|
gpuMem = string(payload[4 : 4+gpuMemLen])
|
|
}
|
|
return &ResourceRequest{CPU: cpu, MemoryGB: mem, GPU: gpu, GPUMemory: gpuMem}, nil
|
|
}
|
|
|
|
// JSONResponseBuilder helps build JSON data responses
|
|
type JSONResponseBuilder struct {
|
|
data any
|
|
}
|
|
|
|
// NewJSONResponseBuilder creates a new JSON response builder
|
|
func NewJSONResponseBuilder(data any) *JSONResponseBuilder {
|
|
return &JSONResponseBuilder{data: data}
|
|
}
|
|
|
|
// Build marshals the data to JSON
|
|
func (b *JSONResponseBuilder) Build() ([]byte, error) {
|
|
return json.Marshal(b.data)
|
|
}
|
|
|
|
// BuildOrEmpty marshals the data to JSON or returns empty array on error
|
|
func (b *JSONResponseBuilder) BuildOrEmpty() []byte {
|
|
data, err := json.Marshal(b.data)
|
|
if err != nil {
|
|
return []byte("[]")
|
|
}
|
|
return data
|
|
}
|
|
|
|
// StringPtr returns a pointer to a string
|
|
func StringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// IntPtr returns a pointer to an int
|
|
func IntPtr(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
// MarshalJSONOrEmpty marshals data to JSON or returns empty array on error
|
|
func MarshalJSONOrEmpty(data any) []byte {
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return []byte("[]")
|
|
}
|
|
return b
|
|
}
|
|
|
|
// MarshalJSONBytes marshals data to JSON bytes with error handling
|
|
func MarshalJSONBytes(data any) ([]byte, error) {
|
|
return json.Marshal(data)
|
|
}
|
|
|
|
// IsEmptyJSON checks if JSON data is empty or "null"
|
|
func IsEmptyJSON(data []byte) bool {
|
|
if len(data) == 0 {
|
|
return true
|
|
}
|
|
// Check for "null", "[]", "{}" or empty after trimming
|
|
s := strings.TrimSpace(string(data))
|
|
return s == "" || s == "null" || s == "[]" || s == "{}"
|
|
}
|