Files
yms-daemon/internal/backendupdate/restart.go
T

147 lines
5.0 KiB
Go

package backendupdate
import (
"context"
"crypto/rand"
"errors"
"fmt"
"os"
"path/filepath"
"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) {
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)
}