refactor(repository): 收敛 model/repository 分层为唯一持久化入口

将 OpenFlare 与平台业务的数据访问从 model 与 apps 直连迁入 repository,
model 仅保留实体与无 IO 规则;补充 code-check 架构守卫与开发规范。
This commit is contained in:
ryan
2026-07-24 17:00:17 +08:00
parent 23a5488203
commit 943818f7d4
184 changed files with 5592 additions and 4364 deletions
+13 -21
View File
@@ -17,11 +17,10 @@ import (
"unicode"
"unicode/utf8"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
@@ -213,8 +212,7 @@ func unsafeGitHubInputRune(character rune) bool {
}
func updateGitHubSourceTx(tx *gorm.DB, projectID uint, input SourceUpdateInput) (bool, error) {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
if _, err := repository.LockPagesProjectByIDTx(tx, projectID); err != nil {
return false, err
}
existing, hasExisting, err := loadProjectSourceForUpdate(tx, projectID)
@@ -231,16 +229,15 @@ func updateGitHubSourceTx(tx *gorm.DB, projectID uint, input SourceUpdateInput)
if !githubSourceConfigChanged(existing, config) {
return false, nil
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", existing.ID).First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, existing.ID)
if err != nil {
return false, err
}
identityChanged := existing.SourceIdentity != config.SourceIdentity
if err := tx.Model(existing).Updates(githubSourceUpdates(config, existing.ConfigVersion+1)).Error; err != nil {
if err := repository.UpdatePagesProjectSourceTx(tx, existing, githubSourceUpdates(config, existing.ConfigVersion+1)); err != nil {
return false, err
}
if err := resetRuntimeAfterGitHubUpdate(tx, &runtime, config, identityChanged); err != nil {
if err := resetRuntimeAfterGitHubUpdate(tx, runtime, config, identityChanged); err != nil {
return false, err
}
return true, nil
@@ -259,7 +256,7 @@ func createGitHubSourceTx(tx *gorm.DB, projectID uint, config githubSourceConfig
ConfigVersion: 1,
SourceIdentity: config.SourceIdentity,
}
if err := tx.Create(source).Error; err != nil {
if err := repository.CreatePagesProjectSourceTx(tx, source); err != nil {
return err
}
runtime := &model.PagesProjectSourceRuntime{SourceID: source.ID, SyncStatus: pagesSourceStatusIdle}
@@ -267,7 +264,7 @@ func createGitHubSourceTx(tx *gorm.DB, projectID uint, config githubSourceConfig
next := nextGitHubCheckAt(time.Now(), source.ID, config.CheckInterval)
runtime.NextCheckAt = &next
}
return tx.Create(runtime).Error
return repository.CreatePagesProjectSourceRuntimeTx(tx, runtime)
}
func githubSourceUpdates(config githubSourceConfig, version int) map[string]any {
@@ -308,7 +305,7 @@ func resetRuntimeAfterGitHubUpdate(
next := nextGitHubCheckAt(time.Now(), runtime.SourceID, config.CheckInterval)
nextCheckAt = &next
}
return tx.Model(runtime).Update("next_check_at", nextCheckAt).Error
return repository.UpdatePagesProjectSourceRuntimeFieldTx(tx, runtime, "next_check_at", nextCheckAt)
}
func nextGitHubCheckAt(now time.Time, sourceID uint, intervalMinutes int) time.Time {
@@ -323,8 +320,8 @@ func markInitialCheckDispatchFailed(ctx context.Context, sourceID uint, configVe
sourceRuntimeColumnSyncStatus: pagesSourceStatusFailed,
sourceRuntimeColumnLastError: errPagesSourceInitialCheckWarning,
}
var source model.PagesProjectSource
if err := db.DB(ctx).Where("id = ? AND config_version = ?", sourceID, configVersion).First(&source).Error; err != nil {
source, err := repository.GetPagesProjectSourceByIDAndConfigVersion(ctx, sourceID, configVersion)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
logger.ErrorF(ctx, "[PagesSource] load initial check source snapshot failed: source_id=%d error=%v", sourceID, err)
}
@@ -335,12 +332,7 @@ func markInitialCheckDispatchFailed(ctx context.Context, sourceID uint, configVe
updates["next_check_at"] = &next
}
now := time.Now()
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where("EXISTS (SELECT 1 FROM of_pages_project_sources source WHERE source.id = ? AND source.config_version = ?)", sourceID, configVersion).
Updates(updates)
if result.Error != nil {
logger.ErrorF(ctx, "[PagesSource] mark initial check dispatch failure: source_id=%d error=%v", sourceID, result.Error)
if _, err := repository.MarkPagesSourceInitialCheckDispatchFailed(ctx, sourceID, configVersion, now, updates); err != nil {
logger.ErrorF(ctx, "[PagesSource] mark initial check dispatch failure: source_id=%d error=%v", sourceID, err)
}
}
@@ -13,14 +13,13 @@ import (
"strings"
"time"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/integration/githubrelease"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/Rain-kl/Wavelet/pkg/pagesarchive"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const githubSourceDetailProvider = "github"
@@ -201,7 +200,7 @@ func finishGitHubCheckNotModified(
) (string, string, error) {
var revision string
var status string
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
runtime, now, err := lockOwnedSourceRuntime(tx, snapshot)
if err != nil {
return err
@@ -211,7 +210,7 @@ func finishGitHubCheckNotModified(
updates := githubCheckTerminalUpdates(snapshot, now, result.RetryAt)
updates["etag"] = result.ETag
updates[sourceRuntimeColumnSyncStatus] = status
return tx.Model(runtime).Updates(updates).Error
return repository.UpdatePagesProjectSourceRuntimeTx(tx, runtime, updates)
})
return revision, status, err
}
@@ -223,7 +222,7 @@ func finishGitHubCheckTarget(
target *githubSourceTarget,
) (string, error) {
status := pagesSourceStatusIdle
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
runtime, now, err := lockOwnedSourceRuntime(tx, snapshot)
if err != nil {
return err
@@ -234,7 +233,7 @@ func finishGitHubCheckTarget(
updates["last_seen_revision"] = target.Revision
updates["last_seen_detail"] = target.DetailJSON
updates[sourceRuntimeColumnSyncStatus] = status
return tx.Model(runtime).Updates(updates).Error
return repository.UpdatePagesProjectSourceRuntimeTx(tx, runtime, updates)
})
return status, err
}
@@ -273,9 +272,8 @@ func lockOwnedSourceRuntime(
tx *gorm.DB,
snapshot *sourceExecutionSnapshot,
) (*model.PagesProjectSourceRuntime, time.Time, error) {
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", snapshot.SourceID).First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, snapshot.SourceID)
if err != nil {
return nil, time.Time{}, err
}
now := time.Now()
@@ -283,7 +281,7 @@ func lockOwnedSourceRuntime(
!runtime.LeaseExpiresAt.After(now) {
return nil, time.Time{}, errSourceFinalFence
}
return &runtime, now, nil
return runtime, now, nil
}
func failGitHubCheckLease(
@@ -309,13 +307,13 @@ func failGitHubCheckLease(
} else {
updates[sourceRuntimeColumnNextCheckAt] = nil
}
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", snapshot.SourceID, snapshot.LeaseToken, now).
Updates(updates)
if result.Error != nil {
return result.Error
rows, err := repository.UpdatePagesSourceRuntimeByActiveLease(
ctx, snapshot.SourceID, snapshot.LeaseToken, now, updates,
)
if err != nil {
return err
}
if result.RowsAffected != 1 {
if rows != 1 {
return errSourceFinalFence
}
return nil
@@ -338,11 +336,11 @@ func targetRuntimeStatus(
}
func preflightGitHubSyncConfirmation(ctx context.Context, sourceID uint, confirmedRevision string) error {
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).Where("source_id = ?", sourceID).First(&runtime).Error; err != nil {
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
if err != nil {
return err
}
replacement := sourceHasSameReleaseReplacement(&runtime)
replacement := sourceHasSameReleaseReplacement(runtime)
if replacement && confirmedRevision == "" {
return errors.New(errPagesSourceConfirmationNeeded)
}
@@ -673,13 +671,13 @@ func releaseGitHubSyncWithoutActivation(
sourceRuntimeColumnLeaseToken: "",
sourceRuntimeColumnLeaseExpiresAt: nil,
}
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", snapshot.SourceID, snapshot.LeaseToken, now).
Updates(updates)
if result.Error != nil {
return result.Error
rows, err := repository.UpdatePagesSourceRuntimeByActiveLease(
ctx, snapshot.SourceID, snapshot.LeaseToken, now, updates,
)
if err != nil {
return err
}
if result.RowsAffected != 1 {
if rows != 1 {
return errSourceFinalFence
}
return nil
@@ -16,7 +16,10 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/integration/githubrelease"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/hibiken/asynq"
@@ -100,7 +103,7 @@ func TestGitHubSourceValidationNormalizationAndProviderSwitch(t *testing.T) {
if result.CheckTask == nil || result.CheckTask.Action != sourceActionCheck || result.Warning != "" {
t.Errorf("UpdateSourceAs(GitHub) result = %+v, want initial check receipt without warning", result)
}
execution, err := model.GetTaskExecutionByTaskID(ctx, result.CheckTask.TaskID)
execution, err := repository.GetTaskExecutionByTaskID(ctx, result.CheckTask.TaskID)
if err != nil {
t.Fatalf("GetTaskExecutionByTaskID(%q) error = %v, want nil", result.CheckTask.TaskID, err)
}
@@ -171,6 +174,11 @@ func TestGitHubSourceValidationNormalizationAndProviderSwitch(t *testing.T) {
func TestGitHubSourceSaveSurvivesInitialCheckDispatchFailure(t *testing.T) {
ctx := setupPagesSourceTest(t)
// Isolate from other tests that may leave a global Asynq client registered.
previousClient := task.AsynqClient
task.AsynqClient = nil
t.Cleanup(func() { task.AsynqClient = previousClient })
project := mustCreatePagesSourceProject(t, ctx, "github-dispatch-warning")
result, err := UpdateSourceAs(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeGitHubRelease,
@@ -679,7 +687,7 @@ func TestGitHubSyncActivatesWithMetadataRevisionAndPackageChecksum(t *testing.T)
if outcome == nil || outcome.Deployment == nil || outcome.Stale {
t.Fatalf("syncGitHubSource() = %+v, want active deployment", outcome)
}
deployment, err := model.GetPagesDeploymentByID(ctx, outcome.Deployment.ID)
deployment, err := repository.GetPagesDeploymentByID(ctx, outcome.Deployment.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", outcome.Deployment.ID, err)
}
@@ -878,7 +886,7 @@ func TestGitHubSyncRejectsStaleConfirmationWithoutChangingActive(t *testing.T) {
if err == nil || err.Error() != errPagesSourceConfirmationStale {
t.Errorf("syncGitHubSource(stale confirmation) error = %v, want %q", err, errPagesSourceConfirmationStale)
}
storedProject, loadErr := model.GetPagesProjectByID(ctx, project.ID)
storedProject, loadErr := repository.GetPagesProjectByID(ctx, project.ID)
if loadErr != nil {
t.Fatalf("GetPagesProjectByID() error = %v, want nil", loadErr)
}
+3 -5
View File
@@ -19,7 +19,6 @@ import (
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
@@ -325,11 +324,10 @@ func removePagesUploadIfUnreferenced(ctx context.Context, projectID uint, upload
if uploadID == 0 {
return nil
}
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
if projectID != 0 {
var project model.PagesProject
projectErr := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error
if projectErr != nil && !errors.Is(projectErr, gorm.ErrRecordNotFound) {
if _, projectErr := repository.LockPagesProjectByIDTx(tx, projectID); projectErr != nil &&
!errors.Is(projectErr, gorm.ErrRecordNotFound) {
return projectErr
}
}
+45 -59
View File
@@ -17,8 +17,8 @@ import (
"time"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/Rain-kl/Wavelet/pkg/pagesarchive"
"gorm.io/gorm"
@@ -101,7 +101,7 @@ type View struct {
// ListProjects 列出全部 Pages 项目。
func ListProjects(ctx context.Context) ([]View, error) {
projects, err := model.ListPagesProjects(ctx)
projects, err := repository.ListPagesProjects(ctx)
if err != nil {
return nil, err
}
@@ -118,7 +118,7 @@ func ListProjects(ctx context.Context) ([]View, error) {
// GetProject 获取 Pages 项目详情。
func GetProject(ctx context.Context, id uint) (*View, error) {
project, err := model.GetPagesProjectByID(ctx, id)
project, err := repository.GetPagesProjectByID(ctx, id)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func CreateProject(ctx context.Context, input Input) (*View, error) {
if err != nil {
return nil, err
}
if err = model.CreatePagesProjectRecord(ctx, project); err != nil {
if err = repository.CreatePagesProjectRecord(ctx, project); err != nil {
if isUniqueConstraintError(err) {
return nil, errors.New(errPagesSlugExists)
}
@@ -143,7 +143,7 @@ func CreateProject(ctx context.Context, input Input) (*View, error) {
// UpdateProject 更新 Pages 项目。
func UpdateProject(ctx context.Context, id uint, input Input) (*View, error) {
var project *model.PagesProject
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var existing model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&existing, id).Error; err != nil {
return err
@@ -177,10 +177,7 @@ func UpdateProject(ctx context.Context, id uint, input Input) (*View, error) {
}
if contentConfigChanged {
updates["content_config_version"] = existing.ContentConfigVersion + 1
var source model.PagesProjectSource
sourceErr := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", existing.ID).
First(&source).Error
source, sourceErr := repository.LockPagesProjectSourceByProjectIDTx(tx, existing.ID)
if sourceErr != nil && !errors.Is(sourceErr, gorm.ErrRecordNotFound) {
return sourceErr
}
@@ -220,12 +217,12 @@ func ensureDeploymentEntry(conn *gorm.DB, deploymentID uint, rootDir, entryFile
// DeleteProject 删除 Pages 项目。
func DeleteProject(ctx context.Context, id uint) error {
project, err := model.GetPagesProjectByID(ctx, id)
project, err := repository.GetPagesProjectByID(ctx, id)
if err != nil {
return err
}
var deployments []model.PagesDeployment
err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err = repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var lockedProject model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&lockedProject, project.ID).Error; err != nil {
return err
@@ -241,25 +238,19 @@ func DeleteProject(ctx context.Context, id uint) error {
return errors.New(errPagesDeleteReferenced)
}
}
var source model.PagesProjectSource
sourceErr := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", project.ID).
First(&source).Error
source, sourceErr := repository.LockPagesProjectSourceByProjectIDTx(tx, project.ID)
if sourceErr != nil && !errors.Is(sourceErr, gorm.ErrRecordNotFound) {
return sourceErr
}
if sourceErr == nil {
var runtime model.PagesProjectSourceRuntime
runtimeErr := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", source.ID).
First(&runtime).Error
if runtimeErr != nil && !errors.Is(runtimeErr, gorm.ErrRecordNotFound) {
if _, runtimeErr := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID); runtimeErr != nil &&
!errors.Is(runtimeErr, gorm.ErrRecordNotFound) {
return runtimeErr
}
if err := tx.Where("source_id = ?", source.ID).Delete(&model.PagesProjectSourceRuntime{}).Error; err != nil {
if err := repository.DeletePagesProjectSourceRuntimeBySourceIDTx(tx, source.ID); err != nil {
return err
}
if err := tx.Delete(&source).Error; err != nil {
if err := repository.DeletePagesProjectSourceTx(tx, source); err != nil {
return err
}
}
@@ -291,10 +282,10 @@ func DeleteProject(ctx context.Context, id uint) error {
// ListProjectDeployments 列出项目的全部部署。
func ListProjectDeployments(ctx context.Context, projectID uint) ([]DeploymentView, error) {
if _, err := model.GetPagesProjectByID(ctx, projectID); err != nil {
if _, err := repository.GetPagesProjectByID(ctx, projectID); err != nil {
return nil, err
}
deployments, err := model.ListPagesDeployments(ctx, projectID)
deployments, err := repository.ListPagesDeployments(ctx, projectID)
if err != nil {
return nil, err
}
@@ -307,10 +298,10 @@ func ListProjectDeployments(ctx context.Context, projectID uint) ([]DeploymentVi
// ListDeploymentFiles 列出部署文件清单。
func ListDeploymentFiles(ctx context.Context, deploymentID uint) ([]DeploymentFileView, error) {
if _, err := model.GetPagesDeploymentByID(ctx, deploymentID); err != nil {
if _, err := repository.GetPagesDeploymentByID(ctx, deploymentID); err != nil {
return nil, err
}
files, err := model.ListPagesDeploymentFiles(ctx, deploymentID)
files, err := repository.ListPagesDeploymentFiles(ctx, deploymentID)
if err != nil {
return nil, err
}
@@ -330,7 +321,7 @@ func ListDeploymentFiles(ctx context.Context, deploymentID uint) ([]DeploymentFi
// UploadDeployment 上传 Pages 部署包(本地 multipart 文件)。
func UploadDeployment(ctx context.Context, projectID uint, fileHeader *multipart.FileHeader, createdBy string) (*DeploymentView, error) {
project, err := model.GetPagesProjectByID(ctx, projectID)
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
return nil, err
}
@@ -364,7 +355,7 @@ type UploadFromURLInput struct {
// UploadDeploymentFromURL downloads a package from url and creates a deployment.
func UploadDeploymentFromURL(ctx context.Context, projectID uint, rawURL string, createdBy string) (*DeploymentView, error) {
project, err := model.GetPagesProjectByID(ctx, projectID)
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
return nil, err
}
@@ -438,7 +429,7 @@ func createDeploymentFromTempPackage(
}
}()
deployment := &model.PagesDeployment{}
err = db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err = repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var lockedProject model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&lockedProject, project.ID).Error; err != nil {
return err
@@ -539,7 +530,7 @@ func pruneProjectDeploymentHistory(ctx context.Context, projectID uint, keepCoun
// Returns the number of deployments deleted from the database.
func pruneProjectDeploymentHistoryOnce(ctx context.Context, projectID uint, keepCount int, preserveCandidateID uint) (int, error) {
var deletedDeployments []model.PagesDeployment
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
return fmt.Errorf("load pages project: %w", err)
@@ -705,7 +696,7 @@ type deploymentActivationSource struct {
}
func ensureActivationDeploymentUpload(ctx context.Context, projectID uint, deploymentID uint) error {
deployment, err := model.GetPagesDeploymentByID(ctx, deploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, deploymentID)
if err != nil {
return err
}
@@ -725,7 +716,7 @@ func activateDeploymentTransaction(
now time.Time,
) (deploymentActivationAudit, error) {
audit := deploymentActivationAudit{}
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
return err
@@ -755,23 +746,18 @@ func activateDeploymentTransaction(
}
func lockDeploymentActivationSource(tx *gorm.DB, projectID uint) (*deploymentActivationSource, error) {
var source model.PagesProjectSource
err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", projectID).
First(&source).Error
source, err := repository.LockPagesProjectSourceByProjectIDTx(tx, projectID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", source.ID).
First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID)
if err != nil {
return nil, err
}
return &deploymentActivationSource{Source: &source, Runtime: &runtime}, nil
return &deploymentActivationSource{Source: source, Runtime: runtime}, nil
}
func loadDeploymentActivationTarget(
@@ -821,10 +807,10 @@ func fenceDeploymentActivationSource(
audit.SourceType = state.Source.SourceType
audit.SourceIdentity = state.Source.SourceIdentity
audit.AutoDisabled = state.Source.AutoUpdateEnabled
if err := tx.Model(state.Source).Updates(map[string]any{
if err := repository.UpdatePagesProjectSourceTx(tx, state.Source, map[string]any{
sourceColumnConfigVersion: state.Source.ConfigVersion + 1,
sourceColumnAutoUpdateEnabled: false,
}).Error; err != nil {
}); err != nil {
return err
}
if deployment.SourceIdentity != nil && *deployment.SourceIdentity == state.Source.SourceIdentity &&
@@ -835,13 +821,13 @@ func fenceDeploymentActivationSource(
state.Runtime.LastAppliedRevision = ""
state.Runtime.LastAppliedDetail = ""
}
return tx.Model(state.Runtime).Updates(map[string]any{
return repository.UpdatePagesProjectSourceRuntimeTx(tx, state.Runtime, map[string]any{
"last_applied_revision": state.Runtime.LastAppliedRevision,
"last_applied_detail": state.Runtime.LastAppliedDetail,
sourceRuntimeColumnSyncStatus: normalizedSourceRuntimeStatus(state.Runtime),
sourceRuntimeColumnLeaseToken: "",
sourceRuntimeColumnLeaseExpiresAt: nil,
}).Error
})
}
func switchActiveDeploymentTx(
@@ -867,7 +853,7 @@ func switchActiveDeploymentTx(
// GetDeploymentPackageHash returns the upload SHA-256 hash of the deployment package.
// Prefer GetProjectLatestPackageHash for Agent latest-pointer pulls.
func GetDeploymentPackageHash(ctx context.Context, deploymentID uint) (string, error) {
deployment, err := model.GetPagesDeploymentByID(ctx, deploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, deploymentID)
if err != nil {
return "", err
}
@@ -893,7 +879,7 @@ func GetProjectLatestPackageHash(ctx context.Context, projectID uint) (uint, str
// OpenDeploymentPackage opens the deployment artifact from the upload storage framework.
func OpenDeploymentPackage(ctx context.Context, deploymentID uint) (DeploymentPackage, error) {
deployment, err := model.GetPagesDeploymentByID(ctx, deploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, deploymentID)
if err != nil {
return DeploymentPackage{}, err
}
@@ -926,7 +912,7 @@ func resolveProjectActiveDeploymentForAgent(ctx context.Context, projectID uint)
if projectID == 0 {
return nil, errors.New(errPagesProjectNotFound)
}
project, err := model.GetPagesProjectByID(ctx, projectID)
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
return nil, err
}
@@ -939,7 +925,7 @@ func resolveProjectActiveDeploymentForAgent(ctx context.Context, projectID uint)
if err := ensureProjectInActiveConfig(ctx, project.ID); err != nil {
return nil, err
}
deployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
if err != nil {
return nil, err
}
@@ -957,7 +943,7 @@ func deploymentPackageHash(ctx context.Context, deployment *model.PagesDeploymen
if err := ensureDeploymentUploadRecord(ctx, deployment); err != nil {
return "", err
}
reloaded, err := model.GetPagesDeploymentByID(ctx, deployment.ID)
reloaded, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
if err != nil {
return "", err
}
@@ -1012,7 +998,7 @@ func ensureDeploymentUploadRecord(ctx context.Context, deployment *model.PagesDe
if deployment.UploadID > 0 {
return nil
}
project, err := model.GetPagesProjectByID(ctx, deployment.ProjectID)
project, err := repository.GetPagesProjectByID(ctx, deployment.ProjectID)
if err != nil {
return err
}
@@ -1089,7 +1075,7 @@ func attachLegacyDeploymentUpload(
uploadID uint64,
) (uint64, error) {
winnerUploadID := uint64(0)
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var err error
winnerUploadID, err = attachLegacyDeploymentUploadTx(tx, projectID, deploymentID, uploadID)
return err
@@ -1146,11 +1132,11 @@ func attachLegacyDeploymentUploadTx(
// it is the project's current active deployment and the project is used by the
// active main config.
func ensureDeploymentInActiveSnapshot(ctx context.Context, deploymentID uint) error {
deployment, err := model.GetPagesDeploymentByID(ctx, deploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, deploymentID)
if err != nil {
return err
}
project, err := model.GetPagesProjectByID(ctx, deployment.ProjectID)
project, err := repository.GetPagesProjectByID(ctx, deployment.ProjectID)
if err != nil {
return err
}
@@ -1163,7 +1149,7 @@ func ensureDeploymentInActiveSnapshot(ctx context.Context, deploymentID uint) er
// ensureProjectInActiveConfig checks that the Pages project is referenced by at
// least one pages route in the active main config snapshot.
func ensureProjectInActiveConfig(ctx context.Context, projectID uint) error {
version, err := model.GetActiveConfigVersion(ctx)
version, err := repository.GetActiveConfigVersion(ctx)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errPagesPackageNotInActiveConfig)
@@ -1192,7 +1178,7 @@ func ensureProjectInActiveConfig(ctx context.Context, projectID uint) error {
}
if route.PagesDeployment.DeploymentID != 0 {
// Historical snapshot may only pin deployment_id.
snapDeployment, snapErr := model.GetPagesDeploymentByID(ctx, route.PagesDeployment.DeploymentID)
snapDeployment, snapErr := repository.GetPagesDeploymentByID(ctx, route.PagesDeployment.DeploymentID)
if snapErr != nil {
continue
}
@@ -1239,7 +1225,7 @@ func parseSnapshotRoutes(snapshotJSON string) ([]snapshotRouteRef, error) {
// DeleteDeployment 删除 Pages 部署。
func DeleteDeployment(ctx context.Context, projectID uint, deploymentID uint) error {
var removed model.PagesDeployment
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
return err
@@ -1354,13 +1340,13 @@ func buildProjectView(ctx context.Context, project *model.PagesProject) (*View,
CreatedAt: project.CreatedAt,
UpdatedAt: project.UpdatedAt,
}
count, err := model.CountPagesDeploymentsByProjectID(ctx, project.ID)
count, err := repository.CountPagesDeploymentsByProjectID(ctx, project.ID)
if err != nil {
return nil, err
}
view.DeploymentCount = count
if project.ActiveDeploymentID != nil && *project.ActiveDeploymentID != 0 {
deployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
deployment, err := repository.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
if err == nil {
active := buildDeploymentView(deployment)
view.ActiveDeployment = &active
+11 -11
View File
@@ -243,7 +243,7 @@ func TestUpdateProjectValidatesActiveDeploymentEntry(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), errPagesEntryFileMissing)
stored, err := model.GetPagesProjectByID(ctx, project.ID)
stored, err := repository.GetPagesProjectByID(ctx, project.ID)
require.NoError(t, err)
assert.Equal(t, "dist", stored.RootDir)
assert.Equal(t, "index.html", stored.EntryFile)
@@ -291,7 +291,7 @@ func TestUploadDeploymentStoresPackageInUploadFramework(t *testing.T) {
require.NoError(t, err)
assert.NotZero(t, deployment.UploadID)
storedDeployment, err := model.GetPagesDeploymentByID(ctx, deployment.ID)
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
require.NoError(t, err)
assert.NotZero(t, storedDeployment.UploadID)
assert.Empty(t, storedDeployment.ArtifactPath)
@@ -371,7 +371,7 @@ func TestOpenDeploymentPackageHydratesLegacyArtifactPath(t *testing.T) {
require.Len(t, reader.File, 1)
assert.Equal(t, "index.html", reader.File[0].Name)
storedDeployment, err := model.GetPagesDeploymentByID(ctx, deployment.ID)
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
require.NoError(t, err)
assert.NotZero(t, storedDeployment.UploadID)
assert.Empty(t, storedDeployment.ArtifactPath)
@@ -607,7 +607,7 @@ func TestPruneProjectDeploymentHistory(t *testing.T) {
ids = append(ids, deployment.ID)
}
// After 3 uploads with keep=2 and no active: only 2 newest remain.
deployments, err := model.ListPagesDeployments(ctx, project.ID)
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
assert.Equal(t, ids[2], deployments[0].ID)
@@ -622,11 +622,11 @@ func TestPruneProjectDeploymentHistory(t *testing.T) {
})), "root")
require.NoError(t, err)
deployments, err = model.ListPagesDeployments(ctx, project.ID)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2, "must be at most N=2, not active+N newest")
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
require.NoError(t, err)
require.NotNil(t, storedProject.ActiveDeploymentID)
assert.Equal(t, ids[1], *storedProject.ActiveDeploymentID)
@@ -666,7 +666,7 @@ func TestHistoryCountOnePreservesFreshCandidateUntilActivation(t *testing.T) {
"index.html": "v2",
})), "user:1")
require.NoError(t, err)
deployments, err := model.ListPagesDeployments(ctx, project.ID)
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
@@ -674,7 +674,7 @@ func TestHistoryCountOnePreservesFreshCandidateUntilActivation(t *testing.T) {
"index.html": "v3",
})), "user:1")
require.NoError(t, err)
deployments, err = model.ListPagesDeployments(ctx, project.ID)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
kept := map[uint]bool{}
@@ -690,7 +690,7 @@ func TestHistoryCountOnePreservesFreshCandidateUntilActivation(t *testing.T) {
_, err = ActivateDeployment(ctx, project.ID, newCandidate.ID)
require.NoError(t, err)
deployments, err = model.ListPagesDeployments(ctx, project.ID)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 1)
assert.Equal(t, newCandidate.ID, deployments[0].ID)
@@ -723,7 +723,7 @@ func TestPruneUsesLockTimeNewestCandidateInsteadOfStaleCaller(t *testing.T) {
deleted, err := pruneProjectDeploymentHistoryOnce(ctx, project.ID, 1, staleCandidate.ID)
require.NoError(t, err)
assert.Equal(t, 1, deleted)
deployments, err := model.ListPagesDeployments(ctx, project.ID)
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
kept := map[uint]bool{}
@@ -762,7 +762,7 @@ func TestDeleteDeploymentAndProjectSoftDeleteUnreferencedArtifacts(t *testing.T)
var firstUpload model.Upload
require.NoError(t, db.DB(ctx).First(&firstUpload, first.UploadID).Error)
assert.Equal(t, model.UploadStatusDeleted, firstUpload.Status)
_, err = model.GetPagesProjectByID(ctx, project.ID)
_, err = repository.GetPagesProjectByID(ctx, project.ID)
assert.Error(t, err)
}
@@ -9,8 +9,9 @@ import (
"fmt"
"strings"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/model"
)
// ProjectLatestPackageMetadata describes the active package limits published to Agents.
@@ -33,7 +34,7 @@ func GetProjectLatestPackageMetadata(ctx context.Context, projectID uint) (*Proj
if err := ensureDeploymentUploadRecord(ctx, deployment); err != nil {
return nil, err
}
deployment, err = model.GetPagesDeploymentByID(ctx, deployment.ID)
deployment, err = repository.GetPagesDeploymentByID(ctx, deployment.ID)
if err != nil {
return nil, err
}
+4 -2
View File
@@ -10,6 +10,8 @@ import (
"path"
"strings"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/model"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
"gorm.io/gorm"
@@ -156,7 +158,7 @@ func loadActivePagesProject(ctx context.Context, projectID uint, siteName string
if siteName == "" {
siteName = "pages"
}
project, err := model.GetPagesProjectByID(ctx, projectID)
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
if errorsIsNotFound(err) {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", siteName)
@@ -169,7 +171,7 @@ func loadActivePagesProject(ctx context.Context, projectID uint, siteName string
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", siteName)
}
activeDeployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
activeDeployment, err := repository.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
if err != nil {
if errorsIsNotFound(err) {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", siteName)
@@ -13,6 +13,8 @@ import (
"strconv"
"testing"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/gin-gonic/gin"
@@ -48,7 +50,7 @@ func TestUploadDeploymentHandlerRecordsCurrentUserActor(t *testing.T) {
UploadDeploymentHandler(c)
assert.Equal(t, http.StatusOK, recorder.Code)
deployments, err := model.ListPagesDeployments(t.Context(), project.ID)
deployments, err := repository.ListPagesDeployments(t.Context(), project.ID)
require.NoError(t, err)
require.Len(t, deployments, 1)
assert.Equal(t, "user:42", deployments[0].CreatedBy)
@@ -83,7 +85,7 @@ func TestUploadDeploymentFromURLHandlerRecordsCurrentUserActor(t *testing.T) {
UploadDeploymentFromURLHandler(c)
assert.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
deployments, err := model.ListPagesDeployments(t.Context(), project.ID)
deployments, err := repository.ListPagesDeployments(t.Context(), project.ID)
require.NoError(t, err)
require.Len(t, deployments, 1)
assert.Equal(t, "user:77", deployments[0].CreatedBy)
+30 -46
View File
@@ -15,10 +15,9 @@ import (
"strings"
"time"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
@@ -120,7 +119,7 @@ type remoteSourceConfig struct {
// GetSource returns the current persisted source or a manual discriminator.
func GetSource(ctx context.Context, projectID uint) (*SourceView, error) {
if _, err := model.GetPagesProjectByID(ctx, projectID); err != nil {
if _, err := repository.GetPagesProjectByID(ctx, projectID); err != nil {
return nil, err
}
source, runtime, err := loadSourceByProject(ctx, projectID)
@@ -156,7 +155,7 @@ func UpdateSourceAs(
changed := false
var persistedSource model.PagesProjectSource
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
var err error
switch strings.TrimSpace(input.SourceType) {
case PagesSourceTypeRemoteURL:
@@ -169,7 +168,12 @@ func UpdateSourceAs(
if err != nil || !changed || strings.TrimSpace(input.SourceType) != PagesSourceTypeGitHubRelease {
return err
}
return tx.Where("project_id = ?", projectID).First(&persistedSource).Error
source, loadErr := repository.LockPagesProjectSourceByProjectIDTx(tx, projectID)
if loadErr != nil {
return loadErr
}
persistedSource = *source
return nil
})
if err != nil {
return nil, err
@@ -193,8 +197,7 @@ func UpdateSourceAs(
}
func updateRemoteSourceTx(tx *gorm.DB, projectID uint, input SourceUpdateInput) (bool, error) {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
if _, err := repository.LockPagesProjectByIDTx(tx, projectID); err != nil {
return false, err
}
existing, hasExisting, err := loadProjectSourceForUpdate(tx, projectID)
@@ -212,17 +215,14 @@ func updateRemoteSourceTx(tx *gorm.DB, projectID uint, input SourceUpdateInput)
}
func loadProjectSourceForUpdate(tx *gorm.DB, projectID uint) (*model.PagesProjectSource, bool, error) {
var source model.PagesProjectSource
err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", projectID).
First(&source).Error
source, err := repository.LockPagesProjectSourceByProjectIDTx(tx, projectID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return &source, false, nil
return &model.PagesProjectSource{}, false, nil
}
if err != nil {
return nil, false, err
}
return &source, true, nil
return source, true, nil
}
func buildRemoteSourceConfig(
@@ -256,13 +256,13 @@ func createRemoteSourceTx(tx *gorm.DB, projectID uint, config remoteSourceConfig
ConfigVersion: 1,
SourceIdentity: config.Identity,
}
if err := tx.Create(source).Error; err != nil {
if err := repository.CreatePagesProjectSourceTx(tx, source); err != nil {
return err
}
return tx.Create(&model.PagesProjectSourceRuntime{
return repository.CreatePagesProjectSourceRuntimeTx(tx, &model.PagesProjectSourceRuntime{
SourceID: source.ID,
SyncStatus: pagesSourceStatusIdle,
}).Error
})
}
func updateExistingRemoteSourceTx(
@@ -273,14 +273,12 @@ func updateExistingRemoteSourceTx(
if !remoteSourceConfigChanged(existing, config) {
return false, nil
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", existing.ID).
First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, existing.ID)
if err != nil {
return false, err
}
identityChanged := existing.SourceIdentity != config.Identity
if err := tx.Model(existing).Updates(map[string]any{
if err := repository.UpdatePagesProjectSourceTx(tx, existing, map[string]any{
"source_type": PagesSourceTypeRemoteURL,
"remote_url": config.URL,
"allow_insecure": config.AllowInsecure,
@@ -292,10 +290,10 @@ func updateExistingRemoteSourceTx(
"check_interval_minutes": 0,
sourceColumnConfigVersion: existing.ConfigVersion + 1,
"source_identity": config.Identity,
}).Error; err != nil {
}); err != nil {
return false, err
}
return true, resetRuntimeAfterSourceUpdate(tx, &runtime, identityChanged)
return true, resetRuntimeAfterSourceUpdate(tx, runtime, identityChanged)
}
func remoteSourceConfigChanged(existing *model.PagesProjectSource, config remoteSourceConfig) bool {
@@ -312,31 +310,25 @@ func remoteSourceConfigChanged(existing *model.PagesProjectSource, config remote
// DeleteSource idempotently switches a project back to manual mode.
func DeleteSource(ctx context.Context, projectID uint) (*SourceView, error) {
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).First(&project, projectID).Error; err != nil {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
if _, err := repository.LockPagesProjectByIDTx(tx, projectID); err != nil {
return err
}
var source model.PagesProjectSource
err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("project_id = ?", projectID).
First(&source).Error
source, err := repository.LockPagesProjectSourceByProjectIDTx(tx, projectID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", source.ID).
First(&runtime).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
if _, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID); err != nil &&
!errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if err := tx.Where("source_id = ?", source.ID).Delete(&model.PagesProjectSourceRuntime{}).Error; err != nil {
if err := repository.DeletePagesProjectSourceRuntimeBySourceIDTx(tx, source.ID); err != nil {
return err
}
return tx.Delete(&source).Error
return repository.DeletePagesProjectSourceTx(tx, source)
})
if err != nil {
return nil, err
@@ -414,15 +406,7 @@ func remoteSourceIdentity(parsed *url.URL) string {
}
func loadSourceByProject(ctx context.Context, projectID uint) (*model.PagesProjectSource, *model.PagesProjectSourceRuntime, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("project_id = ?", projectID).First(&source).Error; err != nil {
return nil, nil, err
}
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).Where("source_id = ?", source.ID).First(&runtime).Error; err != nil {
return nil, nil, err
}
return &source, &runtime, nil
return repository.GetPagesProjectSourceAndRuntimeByProjectID(ctx, projectID)
}
func buildSourceView(source *model.PagesProjectSource, runtime *model.PagesProjectSourceRuntime) (*SourceView, error) {
@@ -515,7 +499,7 @@ func resetRuntimeAfterSourceUpdate(tx *gorm.DB, runtime *model.PagesProjectSourc
} else {
updates[sourceRuntimeColumnSyncStatus] = normalizedSourceRuntimeStatus(runtime)
}
return tx.Model(runtime).Updates(updates).Error
return repository.UpdatePagesProjectSourceRuntimeTx(tx, runtime, updates)
}
func normalizedSourceRuntimeStatus(runtime *model.PagesProjectSourceRuntime) string {
@@ -11,7 +11,6 @@ import (
"time"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
@@ -62,7 +61,7 @@ func ReconcilePagesOrphanUploads(
}
cutoff := now.UTC().Add(-pagesOrphanUploadIsolation)
systemUser := repository.GetSystemUser(ctx)
candidates, err := model.ListPagesOrphanUploadCandidates(ctx, model.PagesOrphanUploadCandidateQuery{
candidates, err := repository.ListPagesOrphanUploadCandidates(ctx, model.PagesOrphanUploadCandidateQuery{
SystemUserID: systemUser.ID,
UploadType: upload.ReservedPagesDeploymentType,
Marker: pagesIngestMarkerV2,
@@ -130,7 +129,7 @@ func reconcilePagesOrphanUploadCandidate(
outcome := pagesOrphanCleanupSkipped
uploadLocked := false
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
scopeOutcome, proceed, err := lockPagesOrphanCleanupScope(ctx, tx, candidate.ID, marker)
if err != nil {
return err
@@ -179,14 +178,13 @@ func lockPagesOrphanCleanupScope(
return pagesOrphanCleanupSkipped, true, nil
}
var source model.PagesProjectSource
sourceExists, err := lockOptionalPagesCleanupRecord(tx, &source, "id = ?", *marker.SourceID)
source, err := repository.LockPagesProjectSourceByIDTx(tx, *marker.SourceID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return pagesOrphanCleanupSkipped, true, nil
}
if err != nil {
return pagesOrphanCleanupSkipped, false, err
}
if !sourceExists {
return pagesOrphanCleanupSkipped, true, nil
}
if source.ProjectID != marker.ProjectID {
logger.WarnF(ctx,
"[PagesSource] orphan upload source ownership mismatch: upload_id=%d project_id=%d source_id=%d source_project_id=%d",
@@ -198,15 +196,14 @@ func lockPagesOrphanCleanupScope(
return pagesOrphanCleanupInvalidMarker, false, nil
}
var runtime model.PagesProjectSourceRuntime
runtimeExists, err := lockOptionalPagesCleanupRecord(tx, &runtime, "source_id = ?", source.ID)
if err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return pagesOrphanCleanupSkipped, false, err
}
// Read the real clock only after obtaining the runtime row lock. The scanner
// snapshot time is only an isolation cutoff and may be stale after lock wait.
leaseCheckedAt := time.Now()
if runtimeExists && runtime.LeaseExpiresAt != nil && runtime.LeaseExpiresAt.After(leaseCheckedAt) {
if err == nil && runtime.LeaseExpiresAt != nil && runtime.LeaseExpiresAt.After(leaseCheckedAt) {
return pagesOrphanCleanupLeaseBusy, false, nil
}
return pagesOrphanCleanupSkipped, true, nil
+49 -64
View File
@@ -11,10 +11,9 @@ import (
"strings"
"time"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
@@ -89,24 +88,16 @@ func acquireSourceLease(
now := time.Now()
expiresAt := now.Add(leaseDuration)
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where(
"EXISTS (SELECT 1 FROM of_pages_project_sources source WHERE source.id = ? AND source.config_version = ?)",
sourceID,
expectedConfigVersion,
).
Updates(map[string]any{
sourceRuntimeColumnLeaseToken: token,
sourceRuntimeColumnLeaseExpiresAt: expiresAt,
sourceRuntimeColumnSyncStatus: status,
sourceRuntimeColumnLastError: "",
})
if result.Error != nil {
return nil, sourceLeaseStale, result.Error
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 result.RowsAffected == 0 {
if rows == 0 {
outcome, inspectErr := inspectSourceLeaseMiss(ctx, sourceID, expectedConfigVersion, now)
return nil, outcome, inspectErr
}
@@ -129,34 +120,30 @@ func loadSourceExecutionSnapshot(
token string,
) (*sourceExecutionSnapshot, error) {
var snapshot sourceExecutionSnapshot
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
var source model.PagesProjectSource
if err := tx.Where("id = ?", sourceID).First(&source).Error; err != nil {
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
}
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
First(&project, source.ProjectID).Error; err != nil {
project, err := repository.LockPagesProjectByIDTx(tx, source.ProjectID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errSourceLeaseSnapshotStale
}
return err
}
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("id = ?", sourceID).
First(&source).Error; err != nil {
source, err = repository.LockPagesProjectSourceByIDTx(tx, sourceID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errSourceLeaseSnapshotStale
}
return err
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", source.ID).
First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errSourceLeaseSnapshotStale
}
@@ -207,8 +194,8 @@ func inspectSourceLeaseMiss(
expectedConfigVersion int,
now time.Time,
) (sourceLeaseOutcome, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("id = ?", sourceID).First(&source).Error; err != nil {
source, err := repository.GetPagesProjectSourceByID(ctx, sourceID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return sourceLeaseStale, nil
}
@@ -217,8 +204,8 @@ func inspectSourceLeaseMiss(
if source.ConfigVersion != expectedConfigVersion {
return sourceLeaseStale, nil
}
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).Where("source_id = ?", sourceID).First(&runtime).Error; err != nil {
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return sourceLeaseStale, nil
}
@@ -255,13 +242,11 @@ func renewSourceLease(ctx context.Context, snapshot *sourceExecutionSnapshot, du
}
now := time.Now()
expiresAt := now.Add(duration)
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", snapshot.SourceID, snapshot.LeaseToken, now).
Updates(map[string]any{sourceRuntimeColumnLeaseExpiresAt: expiresAt})
if result.Error != nil {
return false, result.Error
rows, err := repository.RenewPagesSourceRuntimeLease(ctx, snapshot.SourceID, snapshot.LeaseToken, now, expiresAt)
if err != nil {
return false, err
}
if result.RowsAffected == 0 {
if rows == 0 {
return false, nil
}
snapshot.LeaseExpiresAt = expiresAt
@@ -274,14 +259,19 @@ func failSourceLease(ctx context.Context, snapshot *sourceExecutionSnapshot, mes
}
message = safeSourceRuntimeError(message)
now := time.Now()
return db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?", snapshot.SourceID, snapshot.LeaseToken, now).
Updates(map[string]any{
_, err := repository.UpdatePagesSourceRuntimeByActiveLease(
ctx,
snapshot.SourceID,
snapshot.LeaseToken,
now,
map[string]any{
sourceRuntimeColumnSyncStatus: pagesSourceStatusFailed,
sourceRuntimeColumnLastError: message,
sourceRuntimeColumnLeaseToken: "",
sourceRuntimeColumnLeaseExpiresAt: nil,
}).Error
},
)
return err
}
func safeSourceRuntimeError(message string) string {
@@ -296,8 +286,8 @@ func safeSourceRuntimeError(message string) string {
}
func sourceLeaseIsBusy(ctx context.Context, sourceID uint) (bool, error) {
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).Where("source_id = ?", sourceID).First(&runtime).Error; err != nil {
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
if err != nil {
return false, err
}
return runtime.LeaseExpiresAt != nil && runtime.LeaseExpiresAt.After(time.Now()), nil
@@ -326,35 +316,30 @@ func recoverExpiredSourceLease(
sourceRuntimeColumnLeaseExpiresAt: nil,
sourceRuntimeColumnNextCheckAt: nextCheckAt,
}
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", sourceID).
Where("lease_token = ?", token).
Where("lease_expires_at = ? AND lease_expires_at <= ?", expiresAt, now).
Where("sync_status = ?", status).
Updates(updates)
if result.Error != nil {
return false, result.Error
rows, err := repository.RecoverExpiredPagesSourceRuntimeLease(
ctx, sourceID, token, expiresAt, status, now, updates,
)
if err != nil {
return false, err
}
return result.RowsAffected == 1, nil
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 {
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", sourceID).
First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, sourceID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
return tx.Model(&runtime).Updates(map[string]any{
return repository.UpdatePagesProjectSourceRuntimeTx(tx, runtime, map[string]any{
sourceRuntimeColumnLeaseToken: "",
sourceRuntimeColumnLeaseExpiresAt: nil,
sourceRuntimeColumnSyncStatus: normalizedSourceRuntimeStatus(&runtime),
}).Error
sourceRuntimeColumnSyncStatus: normalizedSourceRuntimeStatus(runtime),
})
}
func sourceHasSameReleaseReplacement(runtime *model.PagesProjectSourceRuntime) bool {
@@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
)
@@ -286,7 +288,7 @@ func TestSourceConfigAndProjectContentChangesFenceLease(t *testing.T) {
if renewed {
t.Error("renewSourceLease(after content update) = true, want false")
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}
+36 -73
View File
@@ -11,11 +11,10 @@ import (
"fmt"
"time"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"gorm.io/gorm"
)
const (
@@ -65,20 +64,6 @@ type pagesSourceProviderBackoff struct {
RetryAt string `json:"retry_at"`
}
type expiredSourceLeaseCandidate struct {
SourceID uint
LeaseToken string
LeaseExpiresAt time.Time
SyncStatus string
SourceType string
ReleaseSelector string
}
type dueGitHubSourceCandidate struct {
SourceID uint
ConfigVersion int
}
var (
pagesSourceScanNow = time.Now
reconcilePagesSourceOrphans = ReconcilePagesOrphanUploads
@@ -173,17 +158,11 @@ func recoverExpiredPagesSourceLeases(
now time.Time,
summary *pagesSourceScanSummary,
) error {
var candidates []expiredSourceLeaseCandidate
err := db.DB(ctx).
Table("of_pages_project_source_runtime AS runtime").
Select(`runtime.source_id, runtime.lease_token, runtime.lease_expires_at,
runtime.sync_status, source.source_type, source.release_selector`).
Joins("JOIN of_pages_project_sources AS source ON source.id = runtime.source_id").
Where("runtime.lease_token <> ''").
Where("runtime.lease_expires_at IS NOT NULL AND runtime.lease_expires_at <= ?", now).
Where("runtime.sync_status IN ?", []string{pagesSourceStatusChecking, pagesSourceStatusSyncing}).
Order("runtime.source_id ASC").
Scan(&candidates).Error
candidates, err := repository.ListExpiredPagesSourceLeaseCandidates(
ctx,
now,
[]string{pagesSourceStatusChecking, pagesSourceStatusSyncing},
)
if err != nil {
return err
}
@@ -227,27 +206,18 @@ func scanDueGitHubSources(
now time.Time,
summary *pagesSourceScanSummary,
) error {
dueQuery := func() *gorm.DB {
return db.DB(ctx).
Table("of_pages_project_source_runtime AS runtime").
Joins("JOIN of_pages_project_sources AS source ON source.id = runtime.source_id").
Where("source.source_type = ?", PagesSourceTypeGitHubRelease).
Where("source.release_selector = ?", githubReleaseSelectorLatest).
Where("runtime.next_check_at IS NOT NULL AND runtime.next_check_at <= ?", now)
}
var dueCount int64
if err := dueQuery().Count(&dueCount).Error; err != nil {
dueCount, err := repository.CountDueGitHubPagesSourceChecks(
ctx, now, PagesSourceTypeGitHubRelease, githubReleaseSelectorLatest,
)
if err != nil {
return err
}
summary.DueSources = int(dueCount)
var candidates []dueGitHubSourceCandidate
if err := dueQuery().
Select("source.id AS source_id, source.config_version").
Order("runtime.next_check_at ASC").
Order("source.id ASC").
Limit(pagesSourceScanBatchSize).
Scan(&candidates).Error; err != nil {
candidates, err := repository.ListDueGitHubPagesSourceChecks(
ctx, now, PagesSourceTypeGitHubRelease, githubReleaseSelectorLatest, pagesSourceScanBatchSize,
)
if err != nil {
return err
}
summary.SelectedSources = len(candidates)
@@ -261,8 +231,10 @@ func scanDueGitHubSources(
for _, candidate := range candidates {
scanOneDueGitHubSource(ctx, candidate, summary)
}
var remainingDue int64
if err := dueQuery().Count(&remainingDue).Error; err != nil {
remainingDue, err := repository.CountDueGitHubPagesSourceChecks(
ctx, now, PagesSourceTypeGitHubRelease, githubReleaseSelectorLatest,
)
if err != nil {
return err
}
summary.Backlog = int(remainingDue)
@@ -272,7 +244,7 @@ func scanDueGitHubSources(
func scanOneDueGitHubSource(
ctx context.Context,
candidate dueGitHubSourceCandidate,
candidate model.PagesDueGitHubSourceCandidate,
summary *pagesSourceScanSummary,
) {
snapshot, outcome, err := acquireSourceLease(
@@ -343,11 +315,8 @@ func recordPagesSourceProviderBackoff(
}
retryAt := domainError.retryAt
var runtime model.PagesProjectSourceRuntime
if err := db.DB(ctx).
Select("next_check_at").
Where("source_id = ?", sourceID).
First(&runtime).Error; err != nil {
runtime, err := repository.GetPagesProjectSourceRuntimeBySourceID(ctx, sourceID)
if err != nil {
logger.WarnF(ctx, "[PagesSourceScan] load provider backoff deadline failed: source_id=%d error=%v", sourceID, err)
} else if runtime.NextCheckAt != nil {
retryAt = runtime.NextCheckAt
@@ -448,29 +417,23 @@ func recordPagesSourceAutoDispatchFailure(
if retryAt != nil && retryAt.After(next) {
next = retryAt.In(now.Location())
}
result := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ?", snapshot.SourceID).
Where("sync_status = ? AND last_seen_revision = ?", pagesSourceStatusUpdateAvailable, revision).
Where("lease_expires_at IS NULL OR lease_expires_at <= ?", now).
Where(`EXISTS (
SELECT 1 FROM of_pages_project_sources AS source
WHERE source.id = ? AND source.config_version = ?
AND source.source_type = ? AND source.release_selector = ?
AND source.auto_update_enabled = ?
)`,
snapshot.SourceID,
snapshot.SourceConfigVersion,
PagesSourceTypeGitHubRelease,
githubReleaseSelectorLatest,
true,
).
Updates(map[string]any{
rows, err := repository.RecordPagesSourceAutoDispatchFailure(
ctx,
snapshot.SourceID,
snapshot.SourceConfigVersion,
PagesSourceTypeGitHubRelease,
githubReleaseSelectorLatest,
revision,
pagesSourceStatusUpdateAvailable,
now,
map[string]any{
sourceRuntimeColumnSyncStatus: pagesSourceStatusUpdateAvailable,
sourceRuntimeColumnLastError: errPagesSourceTaskDispatchFailed,
sourceRuntimeColumnNextCheckAt: &next,
})
if result.Error != nil {
return false, result.Error
},
)
if err != nil {
return false, err
}
return result.RowsAffected == 1, nil
return rows == 1, nil
}
@@ -16,6 +16,8 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/integration/githubrelease"
"github.com/Rain-kl/Wavelet/internal/model"
@@ -241,7 +243,7 @@ func TestScheduledAutoSyncPersistsExplicitDeploymentTrigger(t *testing.T) {
if err != nil || synced == nil || synced.Deployment == nil || synced.Stale {
t.Fatalf("syncGitHubSourceWithTrigger() = %+v, %v; want active deployment", synced, err)
}
deployment, err := model.GetPagesDeploymentByID(ctx, synced.Deployment.ID)
deployment, err := repository.GetPagesDeploymentByID(ctx, synced.Deployment.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) error = %v", synced.Deployment.ID, err)
}
+23 -35
View File
@@ -16,9 +16,9 @@ import (
"unicode/utf8"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -375,14 +375,7 @@ func findSourceDeployment(
sourceIdentity string,
revision string,
) (*model.PagesDeployment, error) {
var deployment model.PagesDeployment
err := db.DB(ctx).
Where("project_id = ? AND source_identity = ? AND source_revision = ?", projectID, sourceIdentity, revision).
First(&deployment).Error
if err != nil {
return nil, err
}
return &deployment, nil
return repository.GetPagesDeploymentBySourceRevision(ctx, projectID, sourceIdentity, revision)
}
func commitSourceDeploymentWithTrigger(
@@ -408,7 +401,7 @@ func commitSourceDeploymentWithTrigger(
var committed model.PagesDeployment
reused := false
ingestReferenced := false
err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
err := repository.WithPagesTx(ctx, func(tx *gorm.DB) error {
state, err := lockSourceCommitState(tx, snapshot)
if err != nil {
return err
@@ -445,18 +438,15 @@ func commitSourceDeploymentWithTrigger(
func lockSourceCommitState(tx *gorm.DB, snapshot *sourceExecutionSnapshot) (*sourceCommitState, error) {
state := &sourceCommitState{}
var project model.PagesProject
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
First(&project, snapshot.ProjectID).Error; err != nil {
project, err := repository.LockPagesProjectByIDTx(tx, snapshot.ProjectID)
if err != nil {
return nil, sourceFenceRecordError(err)
}
if project.ContentConfigVersion != snapshot.ContentConfigVersion {
return nil, errSourceFinalFence
}
var source model.PagesProjectSource
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("id = ? AND project_id = ?", snapshot.SourceID, snapshot.ProjectID).
First(&source).Error; err != nil {
source, err := repository.LockPagesProjectSourceByIDAndProjectIDTx(tx, snapshot.SourceID, snapshot.ProjectID)
if err != nil {
return nil, sourceFenceRecordError(err)
}
if source.ConfigVersion != snapshot.SourceConfigVersion ||
@@ -464,15 +454,13 @@ func lockSourceCommitState(tx *gorm.DB, snapshot *sourceExecutionSnapshot) (*sou
source.SourceType != snapshot.SourceType {
return nil, errSourceFinalFence
}
var runtime model.PagesProjectSourceRuntime
if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}).
Where("source_id = ?", source.ID).
First(&runtime).Error; err != nil {
runtime, err := repository.LockPagesProjectSourceRuntimeBySourceIDTx(tx, source.ID)
if err != nil {
return nil, sourceFenceRecordError(err)
}
state.Project = &project
state.Source = &source
state.Runtime = &runtime
state.Project = project
state.Source = source
state.Runtime = runtime
if err := refreshSourceCommitLease(state, snapshot); err != nil {
return nil, err
}
@@ -675,13 +663,12 @@ func activateSourceDeploymentTx(
}
nextCheckAt = &next
}
result := tx.Model(&model.PagesProjectSourceRuntime{}).
Where("source_id = ? AND lease_token = ? AND lease_expires_at > ?",
state.Runtime.SourceID,
state.Runtime.LeaseToken,
finishedAt,
).
Updates(map[string]any{
rows, err := repository.UpdatePagesSourceRuntimeByActiveLeaseTx(
tx,
state.Runtime.SourceID,
state.Runtime.LeaseToken,
finishedAt,
map[string]any{
"last_seen_revision": revision,
"last_seen_detail": detailJSON,
"last_applied_revision": revision,
@@ -693,11 +680,12 @@ func activateSourceDeploymentTx(
"next_check_at": nextCheckAt,
sourceRuntimeColumnLeaseToken: "",
sourceRuntimeColumnLeaseExpiresAt: nil,
})
if result.Error != nil {
return result.Error
},
)
if err != nil {
return err
}
if result.RowsAffected != 1 {
if rows != 1 {
return errSourceFinalFence
}
return nil
@@ -17,6 +17,8 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
@@ -70,7 +72,7 @@ func mustCreateActiveManualDeployment(
if _, err := ActivateDeploymentAs(ctx, projectID, view.ID, "user:1"); err != nil {
t.Fatalf("ActivateDeploymentAs(project=%d, deployment=%d) error = %v, want nil", projectID, view.ID, err)
}
deployment, err := model.GetPagesDeploymentByID(ctx, view.ID)
deployment, err := repository.GetPagesDeploymentByID(ctx, view.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", view.ID, err)
}
@@ -120,14 +122,14 @@ func TestSyncRemoteSourceAtomicallyActivatesAndReusesChecksum(t *testing.T) {
if first == nil || first.Stale || first.Reused || first.Deployment == nil {
t.Fatalf("syncRemoteSource(first) = %+v, want new active deployment", first)
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}
if storedProject.ActiveDeploymentID == nil || *storedProject.ActiveDeploymentID != first.Deployment.ID {
t.Fatalf("project ActiveDeploymentID = %v, want %d", storedProject.ActiveDeploymentID, first.Deployment.ID)
}
deployment, err := model.GetPagesDeploymentByID(ctx, first.Deployment.ID)
deployment, err := repository.GetPagesDeploymentByID(ctx, first.Deployment.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", first.Deployment.ID, err)
}
@@ -293,7 +295,7 @@ func TestSyncRemoteSourceFinalFenceCompensatesIngest(t *testing.T) {
if outcome == nil || !outcome.Stale || outcome.Deployment != nil {
t.Fatalf("syncRemoteSource(final fence) = %+v, want stale outcome without deployment", outcome)
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}
@@ -351,7 +353,7 @@ func TestCommitSourceDeploymentRechecksLeaseAfterUploadLocks(t *testing.T) {
if err != nil || first == nil || first.Deployment == nil {
t.Fatalf("syncRemoteSource(seed) = (%+v, %v), want deployment", first, err)
}
deployment, err := model.GetPagesDeploymentByID(ctx, first.Deployment.ID)
deployment, err := repository.GetPagesDeploymentByID(ctx, first.Deployment.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", first.Deployment.ID, err)
}
@@ -403,14 +405,14 @@ func TestCommitSourceDeploymentRechecksLeaseAfterUploadLocks(t *testing.T) {
if nowCalls != 2 {
t.Fatalf("sourceCommitNow calls = %d, want runtime-lock and post-upload checks", nowCalls)
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}
if storedProject.ActiveDeploymentID != nil {
t.Errorf("ActiveDeploymentID after expiry recheck = %v, want nil", storedProject.ActiveDeploymentID)
}
storedDeployment, err := model.GetPagesDeploymentByID(ctx, deployment.ID)
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
if err != nil {
t.Fatalf("GetPagesDeploymentByID(%d) after expiry error = %v, want nil", deployment.ID, err)
}
@@ -479,7 +481,7 @@ func assertPagesSyncFailureState(
wantDeploymentCount int64,
) {
t.Helper()
project, err := model.GetPagesProjectByID(ctx, projectID)
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", projectID, err)
}
@@ -589,7 +591,7 @@ func TestCommitSourceDeploymentRejectsDeletedTargetUpload(t *testing.T) {
if !errors.Is(err, errSourceFinalFence) {
t.Errorf("commitSourceDeployment(deleted upload) error = %v, want %v", err, errSourceFinalFence)
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}
@@ -13,9 +13,9 @@ import (
"strconv"
"strings"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"gorm.io/gorm"
)
@@ -135,8 +135,8 @@ func (h *SourceActionHandler) Execute(ctx context.Context, payload []byte) (*tas
return nil, task.PermanentError(errPagesSourceActionInvalid)
}
var source model.PagesProjectSource
if err := db.DB(ctx).Where("id = ?", input.SourceID).First(&source).Error; err != nil {
source, err := repository.GetPagesProjectSourceByID(ctx, input.SourceID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
task.AppendLog(ctx, "[resolve] 来源已不存在,本次任务跳过")
return &task.TaskResult{Message: errPagesSourceActionStale}, nil
@@ -179,7 +179,7 @@ func (h *SourceActionHandler) Execute(ctx context.Context, payload []byte) (*tas
if input.Action == sourceActionCheck {
return executeGitHubCheckAction(ctx, snapshot)
}
return executeSourceSyncAction(ctx, &source, snapshot, input)
return executeSourceSyncAction(ctx, source, snapshot, input)
}
func executeGitHubCheckAction(ctx context.Context, snapshot *sourceExecutionSnapshot) (*task.TaskResult, error) {
@@ -313,14 +313,14 @@ func dispatchSourceActionByProject(
return nil, errors.New(errPagesSourceActionInvalid)
}
var source model.PagesProjectSource
if err := db.DB(ctx).Where("project_id = ?", projectID).First(&source).Error; err != nil {
source, err := repository.GetPagesProjectSourceByProjectID(ctx, projectID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New(errPagesSourceNotFound)
}
return nil, err
}
if err := validateSourceActionPreflight(ctx, &source, action, targetRevision, confirmedRevision); err != nil {
if err := validateSourceActionPreflight(ctx, source, action, targetRevision, confirmedRevision); err != nil {
return nil, err
}
busy, err := sourceLeaseIsBusy(ctx, source.ID)
@@ -330,7 +330,7 @@ func dispatchSourceActionByProject(
if busy {
return nil, errors.New(errPagesSourceActionBusy)
}
return dispatchSourceActionSnapshot(ctx, source, action, actor, targetRevision, confirmedRevision, "manual")
return dispatchSourceActionSnapshot(ctx, *source, action, actor, targetRevision, confirmedRevision, "manual")
}
func validateSourceActionPreflight(
@@ -416,7 +416,7 @@ func dispatchSourceActionSnapshotWithTrigger(
logger.ErrorF(ctx, "[PagesSource] dispatch action failed: project_id=%d source_id=%d action=%s error=%v", source.ProjectID, source.ID, action, err)
return nil, errors.New(errPagesSourceTaskDispatchFailed)
}
execution, err := model.GetTaskExecutionByTaskID(ctx, taskID)
execution, err := repository.GetTaskExecutionByTaskID(ctx, taskID)
if err != nil {
logger.ErrorF(ctx, "[PagesSource] load dispatched execution failed: source_id=%d task_id=%s error=%v", source.ID, taskID, err)
return nil, errors.New(errPagesSourceTaskDispatchFailed)
+4 -2
View File
@@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
)
@@ -67,7 +69,7 @@ func mustCreatePagesSourceProject(t *testing.T, ctx context.Context, slug string
if err != nil {
t.Fatalf("CreateProject(%q) error = %v, want nil", slug, err)
}
project, err := model.GetPagesProjectByID(ctx, view.ID)
project, err := repository.GetPagesProjectByID(ctx, view.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", view.ID, err)
}
@@ -356,7 +358,7 @@ func TestDeleteSourceIsIdempotentAndKeepsDeploymentState(t *testing.T) {
if sourceCount != 0 || runtimeCount != 0 || deploymentCount != 1 {
t.Errorf("DeleteSource counts = source:%d runtime:%d deployment:%d, want 0, 0, 1", sourceCount, runtimeCount, deploymentCount)
}
storedProject, err := model.GetPagesProjectByID(ctx, project.ID)
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
if err != nil {
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
}