@@ -53,6 +53,97 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) {
5353 })
5454}
5555
56+ // go test -run Test_ShutdownWithContext_PostShutdownHookReceivesError
57+ func Test_ShutdownWithContext_PostShutdownHookReceivesError (t * testing.T ) {
58+ app := New ()
59+ app .Get ("/" , func (c Ctx ) error {
60+ time .Sleep (10 * time .Millisecond )
61+ return c .SendString ("ok" )
62+ })
63+
64+ hookErr := make (chan error , 1 )
65+ app .Hooks ().OnPostShutdown (func (err error ) error {
66+ hookErr <- err
67+ return nil
68+ })
69+
70+ ln := fasthttputil .NewInmemoryListener ()
71+ go func () {
72+ _ = app .Listener (ln , ListenConfig {DisableStartupMessage : true }) //nolint:errcheck // not needed
73+ }()
74+
75+ require .Eventually (t , func () bool {
76+ conn , err := ln .Dial ()
77+ if err == nil {
78+ _ = conn .Close () //nolint:errcheck // not needed
79+ return true
80+ }
81+ return false
82+ }, time .Second , 20 * time .Millisecond , "server failed to become ready" )
83+
84+ // Keep a request in flight so shutdown cannot drain before the 1ns deadline.
85+ conn , err := ln .Dial ()
86+ require .NoError (t , err )
87+ t .Cleanup (func () { _ = conn .Close () }) //nolint:errcheck // not needed
88+ _ , err = conn .Write ([]byte ("GET / HTTP/1.1\r \n Host: example.com\r \n " ))
89+ require .NoError (t , err )
90+
91+ ctx , cancel := context .WithTimeout (context .Background (), time .Nanosecond )
92+ defer cancel ()
93+ shutdownErr := app .ShutdownWithContext (ctx )
94+ require .Error (t , shutdownErr )
95+
96+ select {
97+ case got := <- hookErr :
98+ // The hook must receive the actual shutdown error, not the nil value
99+ // captured when the defer was registered.
100+ require .Equal (t , shutdownErr , got )
101+ case <- time .After (time .Second ):
102+ t .Fatal ("OnPostShutdown hook was not called" )
103+ }
104+ }
105+
106+ // go test -run Test_GracefulShutdown_PostShutdownFiresOnce
107+ func Test_GracefulShutdown_PostShutdownFiresOnce (t * testing.T ) {
108+ app := New ()
109+ app .Get ("/" , func (c Ctx ) error { return c .SendString ("ok" ) })
110+
111+ fires := make (chan error , 8 )
112+ app .Hooks ().OnPostShutdown (func (err error ) error {
113+ fires <- err
114+ return nil
115+ })
116+
117+ ln := fasthttputil .NewInmemoryListener ()
118+ gctx , gcancel := context .WithCancel (context .Background ())
119+ go func () {
120+ _ = app .Listener (ln , ListenConfig {DisableStartupMessage : true , GracefulContext : gctx }) //nolint:errcheck,contextcheck // not needed
121+ }()
122+
123+ require .Eventually (t , func () bool {
124+ conn , err := ln .Dial ()
125+ if err == nil {
126+ _ = conn .Close () //nolint:errcheck // not needed
127+ return true
128+ }
129+ return false
130+ }, time .Second , 20 * time .Millisecond , "server failed to become ready" )
131+
132+ gcancel () // trigger graceful shutdown
133+
134+ // The hook must fire exactly once.
135+ select {
136+ case <- fires :
137+ case <- time .After (2 * time .Second ):
138+ t .Fatal ("OnPostShutdown was not called" )
139+ }
140+ select {
141+ case <- fires :
142+ t .Fatal ("OnPostShutdown fired more than once" )
143+ case <- time .After (300 * time .Millisecond ):
144+ }
145+ }
146+
56147func testGracefulShutdown (t * testing.T , shutdownTimeout time.Duration ) {
57148 t .Helper ()
58149
0 commit comments