e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
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
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
265 lines
6.4 KiB
Go
265 lines
6.4 KiB
Go
// Copyright 2025 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 store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"k8s.io/klog/v2"
|
|
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/types"
|
|
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/utils"
|
|
)
|
|
|
|
type fileStore struct {
|
|
dataDir string
|
|
locks sync.Map // key: taskName, value: *sync.RWMutex
|
|
}
|
|
|
|
func NewFileStore(dataDir string) (TaskStore, error) {
|
|
if dataDir == "" {
|
|
return nil, fmt.Errorf("dataDir cannot be empty")
|
|
}
|
|
|
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create data directory %s: %w", dataDir, err)
|
|
}
|
|
|
|
testFile := filepath.Join(dataDir, ".test")
|
|
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
|
|
return nil, fmt.Errorf("data directory %s is not writable: %w", dataDir, err)
|
|
}
|
|
os.Remove(testFile)
|
|
|
|
klog.InfoS("initialized file store", "dataDir", dataDir)
|
|
|
|
return &fileStore{
|
|
dataDir: dataDir,
|
|
}, nil
|
|
}
|
|
|
|
func (s *fileStore) getTaskLock(name string) *sync.RWMutex {
|
|
val, _ := s.locks.LoadOrStore(name, &sync.RWMutex{})
|
|
return val.(*sync.RWMutex)
|
|
}
|
|
|
|
func (s *fileStore) Create(ctx context.Context, task *types.Task) error {
|
|
if task == nil {
|
|
return fmt.Errorf("task cannot be nil")
|
|
}
|
|
if task.Name == "" {
|
|
return fmt.Errorf("task name cannot be empty")
|
|
}
|
|
|
|
mu := s.getTaskLock(task.Name)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
taskDir, err := utils.SafeJoin(s.dataDir, task.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid task name: %w", err)
|
|
}
|
|
|
|
if _, err := os.Stat(taskDir); err == nil {
|
|
return fmt.Errorf("task %s already exists", task.Name)
|
|
}
|
|
|
|
if err := os.MkdirAll(taskDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create task directory: %w", err)
|
|
}
|
|
|
|
if err := s.writeTaskFile(taskDir, task); err != nil {
|
|
os.RemoveAll(taskDir)
|
|
return err
|
|
}
|
|
|
|
klog.InfoS("created task", "name", task.Name, "dir", taskDir)
|
|
return nil
|
|
}
|
|
|
|
func (s *fileStore) Update(ctx context.Context, task *types.Task) error {
|
|
if task == nil {
|
|
return fmt.Errorf("task cannot be nil")
|
|
}
|
|
if task.Name == "" {
|
|
return fmt.Errorf("task name cannot be empty")
|
|
}
|
|
|
|
mu := s.getTaskLock(task.Name)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
taskDir, err := utils.SafeJoin(s.dataDir, task.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid task name: %w", err)
|
|
}
|
|
|
|
// Check if task exists
|
|
if _, err := os.Stat(taskDir); os.IsNotExist(err) {
|
|
return fmt.Errorf("task %s does not exist", task.Name)
|
|
}
|
|
|
|
if err := s.writeTaskFile(taskDir, task); err != nil {
|
|
return err
|
|
}
|
|
|
|
klog.V(2).InfoS("updated task", "name", task.Name, "state", task.Status.State)
|
|
return nil
|
|
}
|
|
|
|
func (s *fileStore) Get(ctx context.Context, name string) (*types.Task, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("task name cannot be empty")
|
|
}
|
|
|
|
mu := s.getTaskLock(name)
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
taskDir, err := utils.SafeJoin(s.dataDir, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid task name: %w", err)
|
|
}
|
|
|
|
// Check if task exists
|
|
if _, err := os.Stat(taskDir); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("task %s not found", name)
|
|
}
|
|
|
|
return s.readTaskFile(taskDir, name)
|
|
}
|
|
|
|
func (s *fileStore) List(ctx context.Context) ([]*types.Task, error) {
|
|
entries, err := os.ReadDir(s.dataDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read data directory: %w", err)
|
|
}
|
|
|
|
tasks := make([]*types.Task, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
taskName := entry.Name()
|
|
taskDir, err := utils.SafeJoin(s.dataDir, taskName)
|
|
if err != nil {
|
|
klog.ErrorS(err, "invalid task directory, skipping", "name", taskName)
|
|
continue
|
|
}
|
|
|
|
mu := s.getTaskLock(taskName)
|
|
mu.RLock()
|
|
task, err := s.readTaskFile(taskDir, taskName)
|
|
mu.RUnlock()
|
|
|
|
if err != nil {
|
|
klog.ErrorS(err, "failed to read task, skipping", "name", taskName)
|
|
continue
|
|
}
|
|
|
|
tasks = append(tasks, task)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
func (s *fileStore) Delete(ctx context.Context, name string) error {
|
|
if name == "" {
|
|
return fmt.Errorf("task name cannot be empty")
|
|
}
|
|
|
|
mu := s.getTaskLock(name)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
taskDir, err := utils.SafeJoin(s.dataDir, name)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid task name: %w", err)
|
|
}
|
|
|
|
// Check if task exists
|
|
if _, err := os.Stat(taskDir); os.IsNotExist(err) {
|
|
klog.InfoS("task already deleted", "name", name)
|
|
return nil
|
|
}
|
|
|
|
if err := os.RemoveAll(taskDir); err != nil {
|
|
return fmt.Errorf("failed to delete task %s: %w", name, err)
|
|
}
|
|
|
|
klog.InfoS("deleted task", "name", name)
|
|
return nil
|
|
}
|
|
|
|
func (s *fileStore) getTaskFilePath(taskDir string) string {
|
|
return filepath.Join(taskDir, "task.json")
|
|
}
|
|
|
|
// writeTaskFile writes task data to disk atomically
|
|
func (s *fileStore) writeTaskFile(taskDir string, task *types.Task) error {
|
|
data, err := json.MarshalIndent(task, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal task: %w", err)
|
|
}
|
|
|
|
taskFile := s.getTaskFilePath(taskDir)
|
|
tmpFile := taskFile + ".tmp"
|
|
|
|
if err := os.WriteFile(tmpFile, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
f, err := os.Open(tmpFile)
|
|
if err != nil {
|
|
os.Remove(tmpFile)
|
|
return fmt.Errorf("failed to open temp file for sync: %w", err)
|
|
}
|
|
if err := f.Sync(); err != nil {
|
|
f.Close()
|
|
os.Remove(tmpFile)
|
|
return fmt.Errorf("failed to sync temp file: %w", err)
|
|
}
|
|
f.Close()
|
|
|
|
if err := os.Rename(tmpFile, taskFile); err != nil {
|
|
os.Remove(tmpFile)
|
|
return fmt.Errorf("failed to rename temp file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *fileStore) readTaskFile(taskDir, taskName string) (*types.Task, error) {
|
|
taskFile := s.getTaskFilePath(taskDir)
|
|
|
|
data, err := os.ReadFile(taskFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read task file: %w", err)
|
|
}
|
|
|
|
var task types.Task
|
|
if err := json.Unmarshal(data, &task); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal task file: %w", err)
|
|
}
|
|
|
|
return &task, nil
|
|
}
|