Skip to content

Commit d2a65f9

Browse files
committed
Set limit on pulp orphan cleanup tasks
Set global cap and add new constant: maxGetTaskAttempts (default is 5). HMS-10580 API - pulp orphan cleanup runs too many tasks
1 parent a9e86f6 commit d2a65f9

3 files changed

Lines changed: 106 additions & 13 deletions

File tree

pkg/clients/pulp_client/task.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const (
2323
FAILED string = "failed"
2424
)
2525

26+
// maxGetTaskAttempts is how many consecutive failed GetTask calls PollTask tolerates before
27+
// returning an error from a poll iteration (with SleepWithBackoff between attempts).
28+
const maxGetTaskAttempts = 5
29+
2630
// GetTask Fetch a pulp task
2731
func (r pulpDaoImpl) GetTask(ctx context.Context, taskHref string) (zest.TaskResponse, error) {
2832
ctx, client, err := getZestClient(ctx)
@@ -67,14 +71,23 @@ func (r pulpDaoImpl) CancelTask(ctx context.Context, taskHref string) (zest.Task
6771
// PollTask Poll a task and return the final task object
6872
func (r pulpDaoImpl) PollTask(ctx context.Context, taskHref string) (*zest.TaskResponse, error) {
6973
var task zest.TaskResponse
70-
var err error
7174
inProgress := true
7275
pollCount := 1
7376
logger := zerolog.Ctx(ctx)
7477
for inProgress {
75-
task, err = r.GetTask(ctx, taskHref)
76-
if err != nil {
77-
return nil, err
78+
var err error
79+
for attempt := 1; attempt <= maxGetTaskAttempts; attempt++ {
80+
task, err = r.GetTask(ctx, taskHref)
81+
if err == nil {
82+
break
83+
}
84+
if attempt == maxGetTaskAttempts {
85+
return nil, err
86+
}
87+
logger.Debug().Err(err).Int("attempt", attempt).Str("task_href", taskHref).
88+
Msg("GetTask failed while polling pulp task; retrying")
89+
SleepWithBackoff(pollCount)
90+
pollCount++
7891
}
7992
taskState := *task.State
8093
switch {

pkg/external_repos/commands/cleanup.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"slices"
77
"strings"
88
"sync"
9+
"sync/atomic"
910
"time"
1011

1112
"github.com/content-services/content-sources-backend/pkg/clients/pulp_client"
@@ -116,7 +117,8 @@ func enqueueSnapshotsCleanup(ctx context.Context, olderThanDays int, batchSize i
116117
return fmt.Errorf("error getting repository configurations: %v", err)
117118
}
118119
if batchSize > 0 {
119-
repoConfigs = repoConfigs[:batchSize] // Limit to batch size
120+
limit := min(batchSize, len(repoConfigs))
121+
repoConfigs = repoConfigs[:limit]
120122
}
121123
log.Info().Msgf("Snapshot cleanup: processing %d repositories with outdated snapshots", len(repoConfigs))
122124

@@ -143,12 +145,15 @@ func enqueueSnapshotCleanupForRepoConfig(ctx context.Context, taskClient client.
143145
slices.SortFunc(snaps, func(s1, s2 models.Snapshot) int {
144146
return s1.CreatedAt.Compare(s2.CreatedAt)
145147
})
148+
149+
keepIndex := len(snaps) - 1
150+
cutoffTime := time.Now().Add(-time.Duration(olderThanDays) * 24 * time.Hour)
146151
toBeDeletedSnapUUIDs := make([]string, 0, len(snaps))
147152
for i, snap := range snaps {
148-
if i == len(snaps)-1 && len(toBeDeletedSnapUUIDs) == len(snaps)-1 {
153+
if i == keepIndex && len(toBeDeletedSnapUUIDs) == len(snaps)-1 {
149154
break
150155
}
151-
if snap.CreatedAt.Before(time.Now().Add(-time.Duration(olderThanDays) * 24 * time.Hour)) {
156+
if snap.CreatedAt.Before(cutoffTime) {
152157
toBeDeletedSnapUUIDs = append(toBeDeletedSnapUUIDs, snap.UUID)
153158
}
154159
}
@@ -194,15 +199,23 @@ func enqueueSnapshotCleanupForRepoConfig(ctx context.Context, taskClient client.
194199
}
195200

196201
func pulpOrphanCleanup(ctx context.Context, db *gorm.DB, batchSize int) error {
197-
var err error
198202
daoReg := dao.GetDaoRegistry(db)
199203

200204
domains, err := daoReg.Domain.List(ctx)
201205
if err != nil {
202206
log.Panic().Err(err).Msg("orphan cleanup error: error listing orgs")
203207
}
204208

205-
for i := 0; i < len(domains); i += batchSize {
209+
return runPulpOrphanCleanupForDomains(ctx, domains, batchSize, pulp_client.GetPulpClientWithDomain)
210+
}
211+
212+
// runPulpOrphanCleanupForDomains runs orphan cleanup in batches and stops if any PollTask fails
213+
// (PollTask already retries transient GetTask errors).
214+
func runPulpOrphanCleanupForDomains(ctx context.Context, domains []models.Domain, batchSize int, getPulpClient func(domainName string) pulp_client.PulpClient) error {
215+
var abort atomic.Bool
216+
var firstErr error
217+
218+
for i := 0; i < len(domains) && !abort.Load(); i += batchSize {
206219
end := i + batchSize
207220
if end > len(domains) {
208221
end = len(domains)
@@ -218,24 +231,33 @@ func pulpOrphanCleanup(ctx context.Context, db *gorm.DB, batchSize int) error {
218231
wg.Add(1)
219232
go func() {
220233
defer wg.Done()
221-
222-
pulpClient := pulp_client.GetPulpClientWithDomain(domainName)
234+
if abort.Load() {
235+
return
236+
}
237+
pulpClient := getPulpClient(domainName)
223238
cleanupTask, err := pulpClient.OrphanCleanup(ctx)
224239
if err != nil {
225240
logger.Error().Err(err).Msgf("error starting orphan cleanup")
226241
return
227242
}
228243
logger.Info().Str("task_href", cleanupTask).Msgf("running orphan cleanup for org: %v", org)
229244

230-
_, err = pulp_client.GetGlobalPulpClient().PollTask(ctx, cleanupTask)
245+
_, err = pulpClient.PollTask(ctx, cleanupTask)
231246
if err != nil {
232-
logger.Error().Err(err).Msgf("error polling pulp task for orphan cleanup")
247+
logger.Error().Err(err).
248+
Msg("error polling pulp task for orphan cleanup; remaining domains will be skipped")
249+
if abort.CompareAndSwap(false, true) {
250+
firstErr = err
251+
}
233252
return
234253
}
235254
}()
236255
}
237256
wg.Wait()
238257
}
258+
if abort.Load() {
259+
return fmt.Errorf("pulp orphan cleanup aborted after failed task poll: %w", firstErr)
260+
}
239261
return nil
240262
}
241263

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"errors"
6+
"sync/atomic"
7+
"testing"
8+
9+
"github.com/content-services/content-sources-backend/pkg/clients/pulp_client"
10+
"github.com/content-services/content-sources-backend/pkg/models"
11+
"github.com/rs/zerolog"
12+
zlog "github.com/rs/zerolog/log"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/mock"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// When poll fails, further batches must not run. batchSize 3 runs three goroutines in parallel per batch.
19+
func TestRunPulpOrphanCleanupForDomains_StopsOnFirstPollFailure(t *testing.T) {
20+
prev := zlog.Logger
21+
zlog.Logger = zerolog.Nop()
22+
t.Cleanup(func() { zlog.Logger = prev })
23+
24+
ctx := context.Background()
25+
domains := []models.Domain{
26+
{OrgId: "1", DomainName: "d1"},
27+
{OrgId: "2", DomainName: "d2"},
28+
{OrgId: "3", DomainName: "d3"},
29+
{OrgId: "4", DomainName: "d4"},
30+
{OrgId: "5", DomainName: "d5"},
31+
{OrgId: "6", DomainName: "d6"},
32+
{OrgId: "7", DomainName: "d7"},
33+
{OrgId: "8", DomainName: "d8"},
34+
{OrgId: "9", DomainName: "d9"},
35+
}
36+
const batchSize = 3
37+
taskHref := "https://pulp.example/api/v3/tasks/fake-href/"
38+
39+
var orphanStarts atomic.Int32
40+
mockPulp := pulp_client.NewMockPulpClient(t)
41+
mockPulp.On("OrphanCleanup", mock.Anything).Return(taskHref, nil).Run(func(mock.Arguments) {
42+
orphanStarts.Add(1)
43+
})
44+
mockPulp.On("PollTask", mock.Anything, taskHref).Return(nil, errors.New("injected poll failure"))
45+
46+
getPulpClient := func(_ string) pulp_client.PulpClient {
47+
return mockPulp
48+
}
49+
50+
err := runPulpOrphanCleanupForDomains(ctx, domains, batchSize, getPulpClient)
51+
require.Error(t, err)
52+
assert.Contains(t, err.Error(), "pulp orphan cleanup aborted")
53+
assert.Contains(t, err.Error(), "failed task poll")
54+
55+
starts := orphanStarts.Load()
56+
assert.GreaterOrEqual(t, starts, int32(1), "at least one domain in the first batch should start orphan cleanup")
57+
assert.LessOrEqual(t, starts, int32(batchSize), "only the first batch should run before abort (domains 4–9 must not be processed)")
58+
}

0 commit comments

Comments
 (0)