From 8f2b335409c70b0afc9a948c7c2c35f04ca58c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Mon, 25 May 2026 10:15:59 +0200 Subject: [PATCH] fix(publish): lock source repos/snapshots on publish update switch Affected endpoint: apiPublishUpdateSwitch (PUT /api/publish/{prefix}/{distribution}). The handler registered only the published repo key as a task resource. The underlying source repos (for local) or snapshots (for snapshot-based published repos) were not locked. Concurrent updates to a source repo or snapshot while a publish-update/switch task was running could produce inconsistent published indexes: Task A: apiPublishUpdateSwitch loads published, reads source repo/snapshot Request B: modifies same source repo or snapshot (add/remove packages, etc) Task A: Update() + Publish() reads stale/modified source -> inconsistent published index, or partial write if source deleted mid-task. Fix: for SourceLocalRepo, iterate published.Sources (component -> source UUID), look up each local repo via localRepoCollection.ByUUID and append string(repo.Key()) to resources. For SourceSnapshot, iterate b.Snapshots, look up each snapshot via snapshotCollection.ByName and append string(snapshot.ResourceKey()) to resources. Task queue now serialises against both the published repo and all its sources. --- api/publish.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/api/publish.go b/api/publish.go index 8ba32fd9..6a1a23d2 100644 --- a/api/publish.go +++ b/api/publish.go @@ -471,6 +471,7 @@ func apiPublishUpdateSwitch(c *gin.Context) { collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() snapshotCollection := collectionFactory.SnapshotCollection() + localRepoCollection := collectionFactory.LocalRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { @@ -478,18 +479,29 @@ func apiPublishUpdateSwitch(c *gin.Context) { return } + resources := []string{string(published.Key())} + if published.SourceKind == deb.SourceLocalRepo { if len(b.Snapshots) > 0 { AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("snapshots shouldn't be given when updating local repo")) return } - } else if published.SourceKind == deb.SourceSnapshot { - for _, snapshotInfo := range b.Snapshots { - _, err2 := snapshotCollection.ByName(snapshotInfo.Name) + for _, uuid := range published.Sources { + repo, err2 := localRepoCollection.ByUUID(uuid) if err2 != nil { AbortWithJSONError(c, http.StatusNotFound, err2) return } + resources = append(resources, string(repo.Key())) + } + } else if published.SourceKind == deb.SourceSnapshot { + for _, snapshotInfo := range b.Snapshots { + snapshot, err2 := snapshotCollection.ByName(snapshotInfo.Name) + if err2 != nil { + AbortWithJSONError(c, http.StatusNotFound, err2) + return + } + resources = append(resources, string(snapshot.ResourceKey())) } } else { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unknown published repository type")) @@ -498,7 +510,6 @@ func apiPublishUpdateSwitch(c *gin.Context) { // Field mutations and fresh DB load are deferred to inside the task so // they always operate on a consistent state after the lock is held. - resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { taskCollectionFactory := context.NewCollectionFactory()