Skip to content

Commit 1f157c5

Browse files
committed
Fix builder release and idle reaper retry behavior
1 parent 41f77c3 commit 1f157c5

2 files changed

Lines changed: 108 additions & 26 deletions

File tree

lib/builders/manager.go

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -337,19 +337,24 @@ func (m *manager) ReleaseBuild(ctx context.Context, id string, buildID string) e
337337
if holder != buildID {
338338
return fmt.Errorf("builder %s is acquired by build %s, not %s", id, holder, buildID)
339339
}
340-
delete(m.acquired, id)
341340

342341
// Record usage even for failed builds: last_used_at drives idle TTL.
343342
meta, err := loadMetadata(m.paths, id)
344343
if err != nil {
345344
if errors.Is(err, ErrNotFound) {
345+
delete(m.acquired, id)
346346
return nil // builder was deleted while held
347347
}
348348
return err
349349
}
350350
now := time.Now()
351351
meta.LastUsedAt = &now
352-
return saveMetadata(m.paths, meta)
352+
if err := saveMetadata(m.paths, meta); err != nil {
353+
return err
354+
}
355+
356+
delete(m.acquired, id)
357+
return nil
353358
}
354359

355360
// ResetDisk resets a builder's cache by recreating its disk asynchronously
@@ -586,35 +591,41 @@ func (m *manager) reapIdle(ctx context.Context) {
586591
if err != nil {
587592
continue
588593
}
589-
if meta.Status != StatusReady {
590-
continue
591-
}
592-
if _, held := m.acquired[id]; held {
593-
continue
594-
}
595-
lastActivity := meta.CreatedAt
596-
if meta.LastUsedAt != nil {
597-
lastActivity = *meta.LastUsedAt
598-
}
599-
if lastActivity.After(cutoff) {
594+
if meta.Status != StatusReady && meta.Status != StatusDeleting {
600595
continue
601596
}
602597

603-
attached, err := m.diskAttached(ctx, meta.DiskVolumeID)
604-
if err != nil {
605-
m.logger.Error("idle reaper failed to check builder disk", "id", id, "error", err)
606-
continue
607-
}
608-
if attached {
609-
continue
610-
}
598+
if meta.Status == StatusReady {
599+
if _, held := m.acquired[id]; held {
600+
continue
601+
}
602+
lastActivity := meta.CreatedAt
603+
if meta.LastUsedAt != nil {
604+
lastActivity = *meta.LastUsedAt
605+
}
606+
if lastActivity.After(cutoff) {
607+
continue
608+
}
611609

612-
m.logger.Info("deleting idle builder", "id", id, "last_activity", lastActivity)
613-
meta.Status = StatusDeleting
614-
if err := saveMetadata(m.paths, meta); err != nil {
615-
m.logger.Error("idle reaper failed to mark builder deleting", "id", id, "error", err)
616-
continue
610+
attached, err := m.diskAttached(ctx, meta.DiskVolumeID)
611+
if err != nil {
612+
m.logger.Error("idle reaper failed to check builder disk", "id", id, "error", err)
613+
continue
614+
}
615+
if attached {
616+
continue
617+
}
618+
619+
m.logger.Info("deleting idle builder", "id", id, "last_activity", lastActivity)
620+
meta.Status = StatusDeleting
621+
if err := saveMetadata(m.paths, meta); err != nil {
622+
m.logger.Error("idle reaper failed to mark builder deleting", "id", id, "error", err)
623+
continue
624+
}
625+
} else {
626+
m.logger.Info("resuming idle builder delete", "id", id)
617627
}
628+
618629
if err := m.volumeManager.DeleteVolume(ctx, meta.DiskVolumeID); err != nil && !errors.Is(err, volumes.ErrNotFound) {
619630
m.logger.Error("idle reaper failed to delete builder disk", "id", id, "error", err)
620631
continue

lib/builders/manager_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ type mockInstanceChecker struct {
2424
deleted []string
2525
}
2626

27+
type flakyDeleteVolumeManager struct {
28+
volumes.Manager
29+
failDelete map[string]int
30+
}
31+
2732
func (m *mockInstanceChecker) GetInstance(ctx context.Context, idOrName string) (*instances.Instance, error) {
2833
if m.getErr != nil {
2934
return nil, m.getErr
@@ -43,6 +48,14 @@ func (m *mockInstanceChecker) DeleteInstance(ctx context.Context, id string) err
4348
return nil
4449
}
4550

51+
func (m *flakyDeleteVolumeManager) DeleteVolume(ctx context.Context, id string) error {
52+
if remaining := m.failDelete[id]; remaining > 0 {
53+
m.failDelete[id] = remaining - 1
54+
return errors.New("transient delete failure")
55+
}
56+
return m.Manager.DeleteVolume(ctx, id)
57+
}
58+
4659
func setupTestManager(t *testing.T, cfg Config) (*manager, volumes.Manager, *mockInstanceChecker, *paths.Paths) {
4760
t.Helper()
4861
p := paths.New(t.TempDir())
@@ -241,6 +254,33 @@ func TestReleaseBuild_StampsLastUsed(t *testing.T) {
241254
assert.WithinDuration(t, time.Now(), *got.LastUsedAt, time.Minute)
242255
}
243256

257+
func TestReleaseBuild_KeepsHoldWhenPersistFails(t *testing.T) {
258+
mgr, _, _, p := setupTestManager(t, Config{})
259+
260+
b, err := mgr.CreateBuilder(context.Background(), CreateBuilderRequest{})
261+
require.NoError(t, err)
262+
263+
_, err = mgr.AcquireForBuild(context.Background(), b.ID, "build-1")
264+
require.NoError(t, err)
265+
266+
require.NoError(t, os.Chmod(p.BuilderDir(b.ID), 0555))
267+
t.Cleanup(func() {
268+
_ = os.Chmod(p.BuilderDir(b.ID), 0755)
269+
})
270+
271+
err = mgr.ReleaseBuild(context.Background(), b.ID, "build-1")
272+
require.Error(t, err)
273+
274+
_, err = mgr.AcquireForBuild(context.Background(), b.ID, "build-2")
275+
assert.ErrorIs(t, err, ErrInUse)
276+
277+
require.NoError(t, os.Chmod(p.BuilderDir(b.ID), 0755))
278+
require.NoError(t, mgr.ReleaseBuild(context.Background(), b.ID, "build-1"))
279+
280+
_, err = mgr.AcquireForBuild(context.Background(), b.ID, "build-2")
281+
require.NoError(t, err)
282+
}
283+
244284
func TestAcquireForBuild_RecreatesMissingDisk(t *testing.T) {
245285
mgr, volumeMgr, _, _ := setupTestManager(t, Config{})
246286

@@ -501,6 +541,37 @@ func TestIdleReaper(t *testing.T) {
501541
assert.NoError(t, err, "acquired builder must not be reaped")
502542
}
503543

544+
func TestIdleReaper_RetriesDeletingBuilder(t *testing.T) {
545+
m, volumeMgr, _, p := setupTestManager(t, Config{IdleTTL: time.Hour})
546+
547+
b, err := m.CreateBuilder(context.Background(), CreateBuilderRequest{})
548+
require.NoError(t, err)
549+
550+
meta, err := loadMetadata(p, b.ID)
551+
require.NoError(t, err)
552+
old := time.Now().Add(-2 * time.Hour)
553+
meta.LastUsedAt = &old
554+
require.NoError(t, saveMetadata(p, meta))
555+
556+
m.volumeManager = &flakyDeleteVolumeManager{
557+
Manager: volumeMgr,
558+
failDelete: map[string]int{b.DiskVolumeID: 1},
559+
}
560+
561+
m.reapIdle(context.Background())
562+
563+
got, err := m.GetBuilder(context.Background(), b.ID)
564+
require.NoError(t, err)
565+
assert.Equal(t, StatusDeleting, got.Status)
566+
567+
m.reapIdle(context.Background())
568+
569+
_, err = m.GetBuilder(context.Background(), b.ID)
570+
assert.ErrorIs(t, err, ErrNotFound)
571+
_, err = volumeMgr.GetVolume(context.Background(), b.DiskVolumeID)
572+
assert.ErrorIs(t, err, volumes.ErrNotFound)
573+
}
574+
504575
func TestValidateBuilderID(t *testing.T) {
505576
assert.NoError(t, ValidateBuilderID("abc"))
506577
assert.NoError(t, ValidateBuilderID("team-cache_1"))

0 commit comments

Comments
 (0)