package backendupdate import ( "context" "crypto/rand" "errors" "fmt" "os" "path/filepath" "yms-daemon/internal/deploymentconfig" "yms-daemon/internal/transaction" "yms-daemon/internal/updatepackage" ) // Restart performs a zero-downtime rotation with the exact release currently // exposed by the compatibility JAR path. func (u *Updater) Restart(ctx context.Context, report ProgressReporter) (transaction.Transaction, error) { if u.config.Backend.Type == deploymentconfig.BackendTypeContainer { return transaction.Transaction{}, errors.New("restart is not implemented for container backend") } reportProgress(report, Progress{Message: "Resolving the current native backend release"}) active, err := u.store.ActiveTransaction(ctx) if err == nil { if active.Service != serviceBackend { return active, &transaction.ActiveTransactionError{TransactionID: active.ID} } var request persistedRequest if decodeErr := decodePersistedRequest(active.Request, &request); decodeErr != nil { return active, decodeErr } if request.InputType != inputTypeCurrentRelease { return active, &transaction.ActiveTransactionError{TransactionID: active.ID} } input, inputErr := u.persistedRestartInput(request, active.IdempotencyKey) if inputErr != nil { return active, inputErr } return u.update(ctx, input, report) } if !errors.Is(err, transaction.ErrNotFound) { return transaction.Transaction{}, err } sourcePath, err := currentReleaseSource(u.config.Backend.ActiveJAR) if err != nil { return transaction.Transaction{}, err } jar, err := updatepackage.OpenDirectNativeJAR(sourcePath) if err != nil { return transaction.Transaction{}, err } releasePath, err := u.restartReleasePath(jar) if err != nil { return transaction.Transaction{}, err } restartID := rand.Text() return u.update(ctx, updateInput{ IdempotencyKey: serviceBackend + ":restart:" + restartID, InputType: inputTypeCurrentRelease, SourcePath: jar.Path, SourceSHA256: jar.SHA256, ArtifactFileName: jar.FileName, ArtifactIdentity: jar.Identity, ReleasePath: releasePath, Materialize: jar.CopyArtifact, }, report) } func currentReleaseSource(activeJAR string) (string, error) { info, err := os.Lstat(activeJAR) if err != nil { return "", fmt.Errorf("inspect active compatibility JAR %s: %w", activeJAR, err) } if info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 { return activeJAR, nil } if info.Mode()&os.ModeSymlink == 0 { return "", fmt.Errorf("active compatibility JAR is neither a direct regular file nor symbolic link: %s", activeJAR) } target, err := os.Readlink(activeJAR) if err != nil { return "", fmt.Errorf("read active compatibility JAR link %s: %w", activeJAR, err) } if !filepath.IsAbs(target) { return "", fmt.Errorf("active compatibility JAR link target must be absolute: %s", target) } resolved, err := filepath.EvalSymlinks(target) if err != nil { return "", fmt.Errorf("resolve active compatibility JAR target %s: %w", target, err) } return resolved, nil } func (u *Updater) restartReleasePath(jar updatepackage.DirectNativeJAR) (string, error) { resolvedReleaseDir, err := filepath.EvalSymlinks(u.config.Backend.ReleaseDir) if err != nil { return "", fmt.Errorf("resolve native backend release directory: %w", err) } relative, err := filepath.Rel(resolvedReleaseDir, jar.Path) if err != nil { return "", fmt.Errorf("compare current JAR with native backend release directory: %w", err) } if relative != "." && filepath.IsLocal(relative) { return relative, nil } return filepath.Join("direct", jar.SHA256[:directReleaseDigestLength], jar.FileName), nil } func (u *Updater) persistedRestartInput(request persistedRequest, idempotencyKey string) (updateInput, error) { materialize, err := u.restartMaterializer(request) if err != nil { return updateInput{}, err } return updateInput{ IdempotencyKey: idempotencyKey, InputType: request.InputType, SourcePath: request.SourcePath, SourceSHA256: request.SourceSHA256, ArtifactFileName: request.ArtifactFileName, ArtifactIdentity: request.ArtifactIdentity, ReleasePath: request.ReleasePath, Materialize: materialize, }, nil } func (u *Updater) restartMaterializer(request persistedRequest) (func(string) error, error) { if info, err := os.Lstat(request.ArtifactPath); err == nil { if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { return nil, fmt.Errorf("persisted backend restart artifact is not a direct regular file: %s", request.ArtifactPath) } return func(string) error { return errors.New("persisted backend restart artifact disappeared during resume") }, nil } else if !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("inspect persisted backend restart artifact: %w", err) } paths := []string{request.SourcePath, filepath.Join(u.config.Backend.ReleaseDir, request.ReleasePath)} for _, sourcePath := range paths { jar, err := updatepackage.OpenDirectNativeJAR(sourcePath) if err != nil { continue } if jar.SHA256 == request.SourceSHA256 && jar.Identity == request.ArtifactIdentity { return jar.CopyArtifact, nil } } return nil, fmt.Errorf("backend restart cannot recover artifact with identity %+v", request.ArtifactIdentity) }