Files
run/runtime/data_targets.go
T
2026-08-26 09:56:43 +08:00

278 lines
11 KiB
Go

package runtime
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"browser.local/run/protocol"
)
const dataTargetSnapshotManifestVersion = 1
type dataTargetSnapshotManifest struct {
Version int `json:"version"`
DataTargetKey string `json:"dataTargetKey"`
Kind string `json:"kind"`
WorkspaceKey string `json:"workspaceKey"`
SourceRootKey string `json:"sourceRootKey"`
SourcePathFingerprint string `json:"sourcePathFingerprint"`
SourceSizeBytes int64 `json:"sourceSizeBytes"`
SourceModUnixNano int64 `json:"sourceModUnixNano"`
SnapshotSizeBytes int64 `json:"snapshotSizeBytes"`
SnapshotChecksum string `json:"snapshotChecksum"`
MaterializedAt time.Time `json:"materializedAt"`
}
type dataTargetMaterializeError struct {
code string
retryable bool
}
func (err dataTargetMaterializeError) Error() string { return err.code }
func newDataTargetMaterializeError(code string, retryable bool) error {
return dataTargetMaterializeError{code: code, retryable: retryable}
}
func sqliteProbeFailureForDataTarget(assignment protocol.RunJobAssignment, err error) LifecycleExecutionResult {
var materializeErr dataTargetMaterializeError
if errors.As(err, &materializeErr) {
return sqliteSchemaProbeFailure(assignment, materializeErr.code, materializeErr.retryable)
}
return sqliteSchemaProbeFailure(assignment, "data_target_unavailable", true)
}
func (worker *Worker) materializeSQLiteProbeDataTarget(ctx context.Context, assignment protocol.RunJobAssignment) (string, error) {
plan, _, ok, err := LoadAutonomousLifecyclePlan(worker.cfg)
if err != nil {
return assignment.TargetKey, newDataTargetMaterializeError("data_target_plan_invalid", false)
}
if !ok || plan == nil || len(plan.DataTargets) == 0 {
return assignment.TargetKey, nil
}
if plan.ServerInstanceID != assignment.ServerInstanceID || plan.RunEndpointID != assignment.RunEndpointID || plan.PluginID != assignment.ExecutionInput.SQLiteSchemaProbe.Binding.PluginID {
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
}
if plan.ProfileKey != "" && assignment.ExecutionInput.WorkspaceScope != "" && plan.ProfileKey != assignment.ExecutionInput.WorkspaceScope {
return assignment.TargetKey, newDataTargetMaterializeError("data_target_scope_mismatch", false)
}
var matched *protocol.RunAutonomousDataTarget
for i := range plan.DataTargets {
target := &plan.DataTargets[i]
if dataTargetMatchesSQLiteProbeAssignment(*target, assignment.TargetKey) {
matched = target
break
}
}
if matched == nil {
return assignment.TargetKey, nil
}
if !dataTargetSupportsPlatform(matched.Platforms, runtime.GOOS) {
return assignment.TargetKey, newDataTargetMaterializeError("data_target_platform_unsupported", false)
}
manifest, err := materializeSQLiteSnapshotDataTarget(ctx, worker.cfg.WorkspaceRoot, assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope, *matched, plan.RuntimeBindings)
if err != nil {
log.Printf("RUN phase=data_target.snapshot status=failed target=%s code=%s", safeOptional(matched.Key), safeOptional(dataTargetErrorCode(err)))
return assignment.TargetKey, err
}
log.Printf("RUN phase=data_target.snapshot status=complete target=%s workspaceKey=%s bytes=%d checksum=%s", safeOptional(matched.Key), safeOptional(matched.WorkspaceKey), manifest.SnapshotSizeBytes, safeOptional(manifest.SnapshotChecksum))
return matched.WorkspaceKey, nil
}
func dataTargetMatchesSQLiteProbeAssignment(target protocol.RunAutonomousDataTarget, assignmentTargetKey string) bool {
if target.Kind != "sqlite.snapshot" || assignmentTargetKey == "" {
return false
}
return target.Key == assignmentTargetKey || target.TransportKey == assignmentTargetKey || target.WorkspaceKey == assignmentTargetKey
}
func materializeSQLiteSnapshotDataTarget(ctx context.Context, workspaceRoot string, serverInstanceID string, profileKey string, target protocol.RunAutonomousDataTarget, bindings map[string]string) (dataTargetSnapshotManifest, error) {
if target.Kind != "sqlite.snapshot" || target.RefreshPolicy != "on-demand-snapshot" || target.MaxBytes <= 0 {
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_invalid", false)
}
sourceRoot := strings.TrimSpace(bindings[target.SourceRootKey])
if sourceRoot == "" {
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_source_unbound", false)
}
source, sourceInfo, err := resolveDataTargetSource(sourceRoot, target.SourcePath, target.MaxBytes)
if err != nil {
return dataTargetSnapshotManifest{}, err
}
scope, err := NewWorkspaceResolver(workspaceRoot).Scope(serverInstanceID, profileKey)
if err != nil {
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_scope_invalid", false)
}
destination, _, err := NewWorkspaceResolver(workspaceRoot).WritableTarget(scope, target.WorkspaceKey)
if err != nil {
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_invalid", false)
}
if err := ensureDirectory(filepath.Dir(destination)); err != nil {
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_unavailable", true)
}
temporary := filepath.Join(filepath.Dir(destination), "."+safeWorkspaceName(filepath.Base(target.WorkspaceKey))+".snapshot.tmp")
_ = os.Remove(temporary)
if err := snapshotSQLiteDatabase(ctx, source, temporary, target.MaxBytes); err != nil {
_ = os.Remove(temporary)
return dataTargetSnapshotManifest{}, err
}
snapshotInfo, err := os.Stat(temporary)
if err != nil || !snapshotInfo.Mode().IsRegular() || snapshotInfo.Size() <= 0 || snapshotInfo.Size() > target.MaxBytes {
_ = os.Remove(temporary)
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_snapshot_invalid", true)
}
checksum, err := fingerprintSQLiteSource(temporary)
if err != nil {
_ = os.Remove(temporary)
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_checksum_failed", true)
}
if err := replaceRegularFile(temporary, destination); err != nil {
_ = os.Remove(temporary)
return dataTargetSnapshotManifest{}, newDataTargetMaterializeError("data_target_workspace_unavailable", true)
}
manifest := dataTargetSnapshotManifest{
Version: dataTargetSnapshotManifestVersion,
DataTargetKey: target.Key,
Kind: target.Kind,
WorkspaceKey: target.WorkspaceKey,
SourceRootKey: target.SourceRootKey,
SourcePathFingerprint: digestValue(target.SourcePath),
SourceSizeBytes: sourceInfo.Size(),
SourceModUnixNano: sourceInfo.ModTime().UnixNano(),
SnapshotSizeBytes: snapshotInfo.Size(),
SnapshotChecksum: checksum,
MaterializedAt: time.Now().UTC(),
}
if err := writeDataTargetSnapshotManifest(workspaceRoot, scope, target.WorkspaceKey, manifest); err != nil {
return dataTargetSnapshotManifest{}, err
}
return manifest, nil
}
func resolveDataTargetSource(sourceRoot string, sourcePath string, maxBytes int64) (string, os.FileInfo, error) {
if strings.TrimSpace(sourceRoot) == "" || strings.TrimSpace(sourcePath) == "" || filepath.IsAbs(sourcePath) || strings.Contains(sourcePath, `\`) || strings.Contains(sourcePath, "..") || !protocol.ValidLogicalFileKey(sourcePath) {
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
}
cleanRoot := filepath.Clean(sourceRoot)
if !filepath.IsAbs(cleanRoot) {
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
}
source := filepath.Join(cleanRoot, filepath.FromSlash(sourcePath))
rel, err := filepath.Rel(cleanRoot, source)
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
}
info, err := os.Lstat(source)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil, newDataTargetMaterializeError("data_target_source_missing", true)
}
return "", nil, newDataTargetMaterializeError("data_target_source_unavailable", true)
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return "", nil, newDataTargetMaterializeError("data_target_source_invalid", false)
}
if info.Size() <= 0 || info.Size() > maxBytes {
return "", nil, newDataTargetMaterializeError("data_target_source_size_invalid", false)
}
return source, info, nil
}
func snapshotSQLiteDatabase(ctx context.Context, source string, destination string, maxBytes int64) error {
snapshotCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
database, err := sql.Open(sqliteSchemaProbeDriver, "file:"+source+"?mode=ro")
if err != nil {
return newDataTargetMaterializeError("data_target_sqlite_open_failed", true)
}
defer database.Close()
database.SetMaxOpenConns(1)
if _, err := database.ExecContext(snapshotCtx, "VACUUM INTO "+quoteSQLiteStringLiteral(destination)); err != nil {
return dataTargetSQLiteError(snapshotCtx, err)
}
info, err := os.Stat(destination)
if err != nil {
return newDataTargetMaterializeError("data_target_snapshot_unavailable", true)
}
if info.Size() <= 0 || info.Size() > maxBytes {
return newDataTargetMaterializeError("data_target_snapshot_limit_exceeded", false)
}
return nil
}
func dataTargetSQLiteError(ctx context.Context, err error) error {
if errors.Is(ctx.Err(), context.Canceled) {
return newDataTargetMaterializeError("data_target_cancelled", true)
}
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return newDataTargetMaterializeError("data_target_timeout", true)
}
lower := strings.ToLower(fmt.Sprint(err))
if strings.Contains(lower, "locked") || strings.Contains(lower, "busy") {
return newDataTargetMaterializeError("data_target_busy", true)
}
return newDataTargetMaterializeError("data_target_snapshot_failed", true)
}
func quoteSQLiteStringLiteral(value string) string {
return `'` + strings.ReplaceAll(value, `'`, `''`) + `'`
}
func replaceRegularFile(source string, destination string) error {
if info, err := os.Lstat(destination); err == nil {
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return fmt.Errorf("destination is not a regular file")
}
if err := os.Remove(destination); err != nil {
return err
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
return os.Rename(source, destination)
}
func writeDataTargetSnapshotManifest(workspaceRoot string, scope string, workspaceKey string, manifest dataTargetSnapshotManifest) error {
manifestKey := workspaceKey + ".snapshot.json"
path, _, err := NewWorkspaceResolver(workspaceRoot).WritableTarget(scope, manifestKey)
if err != nil {
return newDataTargetMaterializeError("data_target_manifest_invalid", false)
}
body, err := json.Marshal(manifest)
if err != nil {
return newDataTargetMaterializeError("data_target_manifest_invalid", false)
}
if err := os.WriteFile(path, body, 0o600); err != nil {
return newDataTargetMaterializeError("data_target_manifest_failed", true)
}
return nil
}
func dataTargetSupportsPlatform(platforms []string, targetOS string) bool {
if len(platforms) == 0 {
return true
}
for _, platform := range platforms {
if platform == targetOS {
return true
}
}
return false
}
func dataTargetErrorCode(err error) string {
var materializeErr dataTargetMaterializeError
if errors.As(err, &materializeErr) {
return materializeErr.code
}
return "data_target_unavailable"
}