mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"Wavelet/openflare/plugins/server/kernel/model"
|
||||
"Wavelet/openflare/plugins/server/kernel/repository"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
pagesSourceCheckLeaseDuration = 2 * time.Minute
|
||||
pagesSourceSyncLeaseDuration = 15 * time.Minute
|
||||
sourceLeaseTokenBytes = 32
|
||||
sourceRuntimeErrorMaxBytes = 512
|
||||
sourceRevisionHexLength = 64
|
||||
|
||||
sourceColumnAutoUpdateEnabled = "auto_update_enabled"
|
||||
sourceColumnConfigVersion = "config_version"
|
||||
sourceRuntimeColumnSyncStatus = "sync_status"
|
||||
sourceRuntimeColumnLastError = "last_error"
|
||||
sourceRuntimeColumnLastCheckedAt = "last_checked_at"
|
||||
sourceRuntimeColumnNextCheckAt = "next_check_at"
|
||||
sourceRuntimeColumnLeaseToken = "lease_token"
|
||||
sourceRuntimeColumnLeaseExpiresAt = "lease_expires_at"
|
||||
pagesDeploymentColumnStatus = "status"
|
||||
)
|
||||
|
||||
type sourceLeaseOutcome string
|
||||
|
||||
const (
|
||||
sourceLeaseAcquired sourceLeaseOutcome = "acquired"
|
||||
sourceLeaseBusy sourceLeaseOutcome = "busy"
|
||||
sourceLeaseStale sourceLeaseOutcome = "stale"
|
||||
)
|
||||
|
||||
// sourceExecutionSnapshot captures every mutable value that can affect archive
|
||||
// validation or the atomic activation decision. The queued payload deliberately
|
||||
// does not carry project content configuration.
|
||||
type sourceExecutionSnapshot struct {
|
||||
ProjectID uint
|
||||
SourceID uint
|
||||
SourceConfigVersion int
|
||||
ContentConfigVersion int
|
||||
SourceType string
|
||||
SourceIdentity string
|
||||
RemoteURL string
|
||||
AllowInsecure bool
|
||||
GitHubRepository string
|
||||
ReleaseSelector string
|
||||
ReleaseTag string
|
||||
AssetName string
|
||||
AutoUpdateEnabled bool
|
||||
CheckIntervalMinutes int
|
||||
ETag string
|
||||
LastSeenRevision string
|
||||
LastSeenDetail string
|
||||
LastAppliedRevision string
|
||||
LastAppliedDetail string
|
||||
RootDir string
|
||||
EntryFile string
|
||||
LeaseToken string
|
||||
LeaseExpiresAt time.Time
|
||||
}
|
||||
|
||||
func acquireSourceLease(
|
||||
ctx context.Context,
|
||||
sourceID uint,
|
||||
expectedConfigVersion int,
|
||||
action string,
|
||||
) (*sourceExecutionSnapshot, sourceLeaseOutcome, error) {
|
||||
leaseDuration, status, err := sourceLeaseParameters(action)
|
||||
if err != nil {
|
||||
return nil, sourceLeaseStale, err
|
||||
}
|
||||
token, err := newSourceLeaseToken()
|
||||
if err != nil {
|
||||
return nil, sourceLeaseStale, err
|
||||
}
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(leaseDuration)
|
||||
|
||||
rows, err := repository.TryAcquirePagesSourceRuntimeLease(ctx, sourceID, expectedConfigVersion, now, map[string]any{
|
||||
sourceRuntimeColumnLeaseToken: token,
|
||||
sourceRuntimeColumnLeaseExpiresAt: expiresAt,
|
||||
sourceRuntimeColumnSyncStatus: status,
|
||||
sourceRuntimeColumnLastError: "",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, sourceLeaseStale, err
|
||||
}
|
||||
if rows == 0 {
|
||||
outcome, inspectErr := inspectSourceLeaseMiss(ctx, sourceID, expectedConfigVersion, now)
|
||||
return nil, outcome, inspectErr
|
||||
}
|
||||
|
||||
snapshot, err := loadSourceExecutionSnapshot(ctx, sourceID, token)
|
||||
if err != nil {
|
||||
if errors.Is(err, errSourceLeaseSnapshotStale) {
|
||||
return nil, sourceLeaseStale, nil
|
||||
}
|
||||
return nil, sourceLeaseStale, err
|
||||
}
|
||||
return snapshot, sourceLeaseAcquired, nil
|
||||
}
|
||||
|
||||
var errSourceLeaseSnapshotStale = errors.New("source lease snapshot stale")
|
||||
|
||||
func loadSourceExecutionSnapshot(
|
||||
ctx context.Context,
|
||||
sourceID uint,
|
||||
token string,
|
||||
) (*sourceExecutionSnapshot, error) {
|
||||
var snapshot sourceExecutionSnapshot
|
||||
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
|
||||
source, err := repository.GetPagesProjectSourceByIDTx(tx, sourceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errSourceLeaseSnapshotStale
|
||||
}
|
||||
return err
|
||||
}
|
||||
project, err := repository.LockPagesProjectByIDTx(tx, source.ProjectID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errSourceLeaseSnapshotStale
|
||||
}
|
||||
return err
|
||||
}
|
||||
source, err = repository.LockPagesProjectSourceByIDTx(tx, sourceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errSourceLeaseSnapshotStale
|
||||
}
|
||||
return err
|
||||
}
|
||||
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errSourceLeaseSnapshotStale
|
||||
}
|
||||
return err
|
||||
}
|
||||
// 必须在 runtime 行锁拿到后重新取时间,避免锁等待跨过
|
||||
// lease expiry 时仍使用事务开始前的旧时间继续执行。
|
||||
now := time.Now()
|
||||
if runtime.LeaseToken != token || runtime.LeaseExpiresAt == nil || !runtime.LeaseExpiresAt.After(now) {
|
||||
return errSourceLeaseSnapshotStale
|
||||
}
|
||||
snapshot = sourceExecutionSnapshot{
|
||||
ProjectID: project.ID,
|
||||
SourceID: source.ID,
|
||||
SourceConfigVersion: source.ConfigVersion,
|
||||
ContentConfigVersion: project.ContentConfigVersion,
|
||||
SourceType: source.SourceType,
|
||||
SourceIdentity: source.SourceIdentity,
|
||||
RemoteURL: source.RemoteURL,
|
||||
AllowInsecure: source.AllowInsecure,
|
||||
GitHubRepository: source.GitHubRepository,
|
||||
ReleaseSelector: source.ReleaseSelector,
|
||||
ReleaseTag: source.ReleaseTag,
|
||||
AssetName: source.AssetName,
|
||||
AutoUpdateEnabled: source.AutoUpdateEnabled,
|
||||
CheckIntervalMinutes: source.CheckIntervalMinutes,
|
||||
ETag: runtime.ETag,
|
||||
LastSeenRevision: runtime.LastSeenRevision,
|
||||
LastSeenDetail: runtime.LastSeenDetail,
|
||||
LastAppliedRevision: runtime.LastAppliedRevision,
|
||||
LastAppliedDetail: runtime.LastAppliedDetail,
|
||||
RootDir: project.RootDir,
|
||||
EntryFile: project.EntryFile,
|
||||
LeaseToken: token,
|
||||
LeaseExpiresAt: *runtime.LeaseExpiresAt,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
func inspectSourceLeaseMiss(
|
||||
ctx context.Context,
|
||||
sourceID uint,
|
||||
expectedConfigVersion int,
|
||||
now time.Time,
|
||||
) (sourceLeaseOutcome, error) {
|
||||
source, err := repository.GetPagesProjectSourceByID(ctx, sourceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return sourceLeaseStale, nil
|
||||
}
|
||||
return sourceLeaseStale, err
|
||||
}
|
||||
if source.ConfigVersion != expectedConfigVersion {
|
||||
return sourceLeaseStale, nil
|
||||
}
|
||||
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return sourceLeaseStale, nil
|
||||
}
|
||||
return sourceLeaseStale, err
|
||||
}
|
||||
if runtime.LeaseExpiresAt != nil && runtime.LeaseExpiresAt.After(now) {
|
||||
return sourceLeaseBusy, nil
|
||||
}
|
||||
return sourceLeaseStale, nil
|
||||
}
|
||||
|
||||
func sourceLeaseParameters(action string) (time.Duration, string, error) {
|
||||
switch action {
|
||||
case sourceActionCheck:
|
||||
return pagesSourceCheckLeaseDuration, pagesSourceStatusChecking, nil
|
||||
case sourceActionSync:
|
||||
return pagesSourceSyncLeaseDuration, pagesSourceStatusSyncing, nil
|
||||
default:
|
||||
return 0, "", errors.New(errPagesSourceActionInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
func newSourceLeaseToken() (string, error) {
|
||||
value := make([]byte, sourceLeaseTokenBytes)
|
||||
if _, err := rand.Read(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
func renewSourceLease(ctx context.Context, snapshot *sourceExecutionSnapshot, duration time.Duration) (bool, error) {
|
||||
if snapshot == nil || snapshot.SourceID == 0 || snapshot.LeaseToken == "" || duration <= 0 {
|
||||
return false, errors.New(errPagesSourceLeaseLost)
|
||||
}
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(duration)
|
||||
rows, err := repository.RenewPagesSourceRuntimeLease(ctx, snapshot.SourceID, snapshot.LeaseToken, now, expiresAt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if rows == 0 {
|
||||
return false, nil
|
||||
}
|
||||
snapshot.LeaseExpiresAt = expiresAt
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func failSourceLease(ctx context.Context, snapshot *sourceExecutionSnapshot, message string) error {
|
||||
if snapshot == nil || snapshot.SourceID == 0 || snapshot.LeaseToken == "" {
|
||||
return nil
|
||||
}
|
||||
message = safeSourceRuntimeError(message)
|
||||
now := time.Now()
|
||||
_, err := repository.UpdatePagesSourceRuntimeByActiveLease(
|
||||
ctx,
|
||||
snapshot.SourceID,
|
||||
snapshot.LeaseToken,
|
||||
now,
|
||||
map[string]any{
|
||||
sourceRuntimeColumnSyncStatus: pagesSourceStatusFailed,
|
||||
sourceRuntimeColumnLastError: message,
|
||||
sourceRuntimeColumnLeaseToken: "",
|
||||
sourceRuntimeColumnLeaseExpiresAt: nil,
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func safeSourceRuntimeError(message string) string {
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
return errPagesSourceSyncFailed
|
||||
}
|
||||
if len(message) > sourceRuntimeErrorMaxBytes {
|
||||
message = message[:sourceRuntimeErrorMaxBytes]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func sourceLeaseIsBusy(ctx context.Context, sourceID uint) (bool, error) {
|
||||
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return runtime.LeaseExpiresAt != nil && runtime.LeaseExpiresAt.After(time.Now()), nil
|
||||
}
|
||||
|
||||
// recoverExpiredSourceLease clears one exact expired lease owner. Matching the
|
||||
// token, observed expiry and status prevents a scanner from overwriting a
|
||||
// worker that renewed or was replaced after the candidate query.
|
||||
func recoverExpiredSourceLease(
|
||||
ctx context.Context,
|
||||
sourceID uint,
|
||||
token string,
|
||||
expiresAt time.Time,
|
||||
status string,
|
||||
now time.Time,
|
||||
nextCheckAt *time.Time,
|
||||
) (bool, error) {
|
||||
if sourceID == 0 || token == "" ||
|
||||
(status != pagesSourceStatusChecking && status != pagesSourceStatusSyncing) {
|
||||
return false, nil
|
||||
}
|
||||
updates := map[string]any{
|
||||
sourceRuntimeColumnSyncStatus: pagesSourceStatusFailed,
|
||||
sourceRuntimeColumnLastError: errPagesSourceLeaseExpired,
|
||||
sourceRuntimeColumnLeaseToken: "",
|
||||
sourceRuntimeColumnLeaseExpiresAt: nil,
|
||||
sourceRuntimeColumnNextCheckAt: nextCheckAt,
|
||||
}
|
||||
rows, err := repository.RecoverExpiredPagesSourceRuntimeLease(
|
||||
ctx, sourceID, token, expiresAt, status, now, updates,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return rows == 1, nil
|
||||
}
|
||||
|
||||
// fenceAndNormalizeRuntime invalidates in-flight work while preserving safe
|
||||
// seen/applied cursors. The caller must already hold the source row lock.
|
||||
func fenceAndNormalizeRuntime(tx *gorm.DB, sourceID uint) error {
|
||||
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, sourceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return repository.UpdatePagesProjectSourceRuntimeTx(tx, runtime, map[string]any{
|
||||
sourceRuntimeColumnLeaseToken: "",
|
||||
sourceRuntimeColumnLeaseExpiresAt: nil,
|
||||
sourceRuntimeColumnSyncStatus: normalizedSourceRuntimeStatus(runtime),
|
||||
})
|
||||
}
|
||||
|
||||
func sourceHasSameReleaseReplacement(runtime *model.PagesProjectSourceRuntime) bool {
|
||||
if runtime == nil || runtime.LastSeenRevision == "" || runtime.LastSeenRevision == runtime.LastAppliedRevision {
|
||||
return false
|
||||
}
|
||||
seen := sourceDetail{}
|
||||
applied := sourceDetail{}
|
||||
if unmarshalSourceDetail(runtime.LastSeenDetail, &seen) != nil ||
|
||||
unmarshalSourceDetail(runtime.LastAppliedDetail, &applied) != nil {
|
||||
return false
|
||||
}
|
||||
return seen.ReleaseID != "" && seen.ReleaseID == applied.ReleaseID
|
||||
}
|
||||
Reference in New Issue
Block a user