@@ -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+
2732func (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+
4659func 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+
244284func 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+
504575func TestValidateBuilderID (t * testing.T ) {
505576 assert .NoError (t , ValidateBuilderID ("abc" ))
506577 assert .NoError (t , ValidateBuilderID ("team-cache_1" ))
0 commit comments