@@ -2,7 +2,9 @@ package cli
22
33import (
44"context"
5+ "fmt"
56"net/url"
7+ "sync"
68"testing"
79"time"
810
@@ -12,6 +14,7 @@ import (
1214
1315"cdr.dev/slog"
1416"cdr.dev/slog/sloggers/slogtest"
17+ "github.com/coder/quartz"
1518
1619"github.com/coder/coder/v2/codersdk"
1720"github.com/coder/coder/v2/testutil"
@@ -68,7 +71,7 @@ func TestCloserStack_Mainline(t *testing.T) {
6871t .Parallel ()
6972ctx := testutil .Context (t ,testutil .WaitShort )
7073logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
71- uut := newCloserStack (ctx ,logger )
74+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
7275closes := new ([]* fakeCloser )
7376fc0 := & fakeCloser {closes :closes }
7477fc1 := & fakeCloser {closes :closes }
@@ -90,7 +93,7 @@ func TestCloserStack_Context(t *testing.T) {
9093ctx ,cancel := context .WithCancel (ctx )
9194defer cancel ()
9295logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
93- uut := newCloserStack (ctx ,logger )
96+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
9497closes := new ([]* fakeCloser )
9598fc0 := & fakeCloser {closes :closes }
9699fc1 := & fakeCloser {closes :closes }
@@ -111,7 +114,7 @@ func TestCloserStack_PushAfterClose(t *testing.T) {
111114t .Parallel ()
112115ctx := testutil .Context (t ,testutil .WaitShort )
113116logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
114- uut := newCloserStack (ctx ,logger )
117+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
115118closes := new ([]* fakeCloser )
116119fc0 := & fakeCloser {closes :closes }
117120fc1 := & fakeCloser {closes :closes }
@@ -134,13 +137,9 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
134137ctx ,cancel := context .WithCancel (testCtx )
135138defer cancel ()
136139logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
137- uut := newCloserStack (ctx ,logger )
138- ac := & asyncCloser {
139- t :t ,
140- ctx :testCtx ,
141- complete :make (chan struct {}),
142- started :make (chan struct {}),
143- }
140+ uut := newCloserStack (ctx ,logger ,quartz .NewMock (t ))
141+ ac := newAsyncCloser (testCtx ,t )
142+ defer ac .complete ()
144143err := uut .push ("async" ,ac )
145144require .NoError (t ,err )
146145cancel ()
@@ -160,11 +159,53 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
160159t .Fatal ("closed before stack was finished" )
161160}
162161
163- // complete the asyncCloser
164- close (ac .complete )
162+ ac .complete ()
165163testutil .RequireRecvCtx (testCtx ,t ,closed )
166164}
167165
166+ func TestCloserStack_Timeout (t * testing.T ) {
167+ t .Parallel ()
168+ ctx := testutil .Context (t ,testutil .WaitShort )
169+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
170+ mClock := quartz .NewMock (t )
171+ trap := mClock .Trap ().TickerFunc ("closerStack" )
172+ defer trap .Close ()
173+ uut := newCloserStack (ctx ,logger ,mClock )
174+ var ac [3 ]* asyncCloser
175+ for i := range ac {
176+ ac [i ]= newAsyncCloser (ctx ,t )
177+ err := uut .push (fmt .Sprintf ("async %d" ,i ),ac [i ])
178+ require .NoError (t ,err )
179+ }
180+ defer func () {
181+ for _ ,a := range ac {
182+ a .complete ()
183+ }
184+ }()
185+
186+ closed := make (chan struct {})
187+ go func () {
188+ defer close (closed )
189+ uut .close (nil )
190+ }()
191+ trap .MustWait (ctx ).Release ()
192+ // top starts right away, but it hangs
193+ testutil .RequireRecvCtx (ctx ,t ,ac [2 ].started )
194+ // timer pops and we start the middle one
195+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
196+ testutil .RequireRecvCtx (ctx ,t ,ac [1 ].started )
197+
198+ // middle one finishes
199+ ac [1 ].complete ()
200+ // bottom starts, but also hangs
201+ testutil .RequireRecvCtx (ctx ,t ,ac [0 ].started )
202+
203+ // timer has to pop twice to time out.
204+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
205+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
206+ testutil .RequireRecvCtx (ctx ,t ,closed )
207+ }
208+
168209type fakeCloser struct {
169210closes * []* fakeCloser
170211err error
@@ -176,10 +217,11 @@ func (c *fakeCloser) Close() error {
176217}
177218
178219type asyncCloser struct {
179- t * testing.T
180- ctx context.Context
181- started chan struct {}
182- complete chan struct {}
220+ t * testing.T
221+ ctx context.Context
222+ started chan struct {}
223+ isComplete chan struct {}
224+ comepleteOnce sync.Once
183225}
184226
185227func (c * asyncCloser )Close ()error {
@@ -188,7 +230,20 @@ func (c *asyncCloser) Close() error {
188230case <- c .ctx .Done ():
189231c .t .Error ("timed out" )
190232return c .ctx .Err ()
191- case <- c .complete :
233+ case <- c .isComplete :
192234return nil
193235}
194236}
237+
238+ func (c * asyncCloser )complete () {
239+ c .comepleteOnce .Do (func () {close (c .isComplete ) })
240+ }
241+
242+ func newAsyncCloser (ctx context.Context ,t * testing.T )* asyncCloser {
243+ return & asyncCloser {
244+ t :t ,
245+ ctx :ctx ,
246+ isComplete :make (chan struct {}),
247+ started :make (chan struct {}),
248+ }
249+ }