@@ -2,10 +2,13 @@ package cmd
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "os"
8+ "os/signal"
79 "strings"
810 "sync"
11+ "syscall"
912 "time"
1013
1114 "github.com/karust/openserp/baidu"
@@ -96,7 +99,7 @@ func serve(cmd *cobra.Command, args []string) {
9699 & rawEngine {name : "yandex" },
97100 & rawEngine {name : "baidu" },
98101 )
99- if err := serv . Listen ( ); err != nil {
102+ if err := listenWithGracefulShutdown ( serv , nil ); err != nil {
100103 logrus .Error (err )
101104 }
102105 return
@@ -117,15 +120,15 @@ func serve(cmd *cobra.Command, args []string) {
117120 baseOpts .IsHeadless = false
118121 }
119122
120- engines , err := buildBrowserEngines (baseOpts , proxyCfg )
123+ engines , closeBrowsers , err := buildBrowserEngines (baseOpts , proxyCfg )
121124 if err != nil {
122125 logrus .Error (err )
123126 return
124127 }
125128
126129 serverOpts := buildServerOptions (corsCfg , proxyCfg )
127130 serv := core .NewServerWithOptions (config .Server .Host , config .Server .Port , serverOpts , engines ... )
128- if err := serv . Listen ( ); err != nil {
131+ if err := listenWithGracefulShutdown ( serv , closeBrowsers ); err != nil {
129132 logrus .Error (err )
130133 }
131134}
@@ -154,6 +157,71 @@ func buildServerOptions(corsCfg core.CORSConfig, proxyCfg core.ProxyConfig) core
154157 }
155158}
156159
160+ const gracefulShutdownTimeout = 30 * time .Second
161+
162+ func listenWithGracefulShutdown (serv * core.Server , onShutdown func () error ) error {
163+ listenErrCh := make (chan error , 1 )
164+ go func () {
165+ listenErrCh <- serv .Listen ()
166+ }()
167+
168+ sigCh := make (chan os.Signal , 1 )
169+ signal .Notify (sigCh , os .Interrupt , syscall .SIGTERM )
170+ defer signal .Stop (sigCh )
171+
172+ select {
173+ case err := <- listenErrCh :
174+ return err
175+ case sig := <- sigCh :
176+ logrus .WithField ("signal" , sig .String ()).Info ("Shutdown signal received, draining traffic" )
177+ }
178+
179+ serv .SetDraining (true )
180+
181+ shutdownErr := serv .ShutdownWithTimeout (gracefulShutdownTimeout )
182+ if isServerNotRunningError (shutdownErr ) {
183+ shutdownErr = nil
184+ }
185+
186+ if onShutdown != nil {
187+ resourceErr := onShutdown ()
188+ if resourceErr != nil {
189+ shutdownErr = errors .Join (shutdownErr , resourceErr )
190+ }
191+ }
192+
193+ if listenErr := waitForListenExit (listenErrCh ); listenErr != nil && ! isExpectedListenShutdownError (listenErr ) {
194+ shutdownErr = errors .Join (shutdownErr , listenErr )
195+ }
196+
197+ return shutdownErr
198+ }
199+
200+ func waitForListenExit (listenErrCh <- chan error ) error {
201+ select {
202+ case err := <- listenErrCh :
203+ return err
204+ case <- time .After (time .Second ):
205+ return nil
206+ }
207+ }
208+
209+ func isExpectedListenShutdownError (err error ) bool {
210+ if err == nil {
211+ return true
212+ }
213+ msg := strings .ToLower (err .Error ())
214+ return strings .Contains (msg , "server closed" ) ||
215+ strings .Contains (msg , "closed network connection" )
216+ }
217+
218+ func isServerNotRunningError (err error ) bool {
219+ if err == nil {
220+ return false
221+ }
222+ return strings .Contains (strings .ToLower (err .Error ()), "server is not running" )
223+ }
224+
157225type browserPool struct {
158226 mu sync.Mutex
159227 base core.BrowserOpts
@@ -193,6 +261,27 @@ func (p *browserPool) get(proxyURL string) (*core.Browser, error) {
193261 return b , nil
194262}
195263
264+ func (p * browserPool ) close () error {
265+ p .mu .Lock ()
266+ browsers := make ([]* core.Browser , 0 , len (p .browser ))
267+ for key , b := range p .browser {
268+ browsers = append (browsers , b )
269+ delete (p .browser , key )
270+ }
271+ p .mu .Unlock ()
272+
273+ var closeErr error
274+ for _ , browser := range browsers {
275+ if browser == nil {
276+ continue
277+ }
278+ if err := browser .Close (); err != nil {
279+ closeErr = errors .Join (closeErr , err )
280+ }
281+ }
282+ return closeErr
283+ }
284+
196285type pooledBrowserEngine struct {
197286 name string
198287 limiter * rate.Limiter
@@ -301,15 +390,15 @@ func browserEngineSpecs() []browserEngineSpec {
301390 }
302391}
303392
304- func buildBrowserEngines (baseOpts core.BrowserOpts , proxyCfg core.ProxyConfig ) ([]core.SearchEngine , error ) {
393+ func buildBrowserEngines (baseOpts core.BrowserOpts , proxyCfg core.ProxyConfig ) ([]core.SearchEngine , func () error , error ) {
305394 pool := newBrowserPool (baseOpts )
306395 specs := browserEngineSpecs ()
307396
308397 engines := make ([]core.SearchEngine , 0 , len (specs ))
309398 for _ , spec := range specs {
310399 policy := resolveEngineProxyPolicy (proxyCfg , spec .name )
311400 if err := validateBrowserProxyPolicy (proxyCfg , policy ); err != nil {
312- return nil , fmt .Errorf ("browser proxy validation failed for engine %s: %w" , spec .name , err )
401+ return nil , nil , fmt .Errorf ("browser proxy validation failed for engine %s: %w" , spec .name , err )
313402 }
314403
315404 opts := spec .opts
@@ -324,7 +413,7 @@ func buildBrowserEngines(baseOpts core.BrowserOpts, proxyCfg core.ProxyConfig) (
324413 })
325414 }
326415
327- return engines , nil
416+ return engines , pool . close , nil
328417}
329418
330419func validateBrowserProxyPolicy (proxyCfg core.ProxyConfig , policy core.ProxyPolicy ) error {
0 commit comments