-
Notifications
You must be signed in to change notification settings - Fork 989
feat: graceful shutdown #1977 #3235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 9 commits
44a6e71
4dcdbdf
da04515
5bb4763
95832f5
36158ce
4adba9a
da642c7
36d89ed
9ef55e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,35 +19,41 @@ package extension | |
|
|
||
| import ( | ||
| "container/list" | ||
| "context" | ||
| ) | ||
|
|
||
| var customShutdownCallbacks = list.New() | ||
|
|
||
| /** | ||
| * AddCustomShutdownCallback | ||
| * you should not make any assumption about the order. | ||
| * For example, if you have more than one callbacks, and you wish the order is: | ||
| * callback1() | ||
| * callback2() | ||
| * ... | ||
| * callbackN() | ||
| * Then you should put then together: | ||
| * func callback() { | ||
| * callback1() | ||
| * callback2() | ||
| * ... | ||
| * callbackN() | ||
| * } | ||
| * I think the order of custom callbacks should be decided by the users. | ||
| * Even though I can design a mechanism to support the ordered custom callbacks, | ||
| * the benefit of that mechanism is low. | ||
| * And it may introduce much complication for another users. | ||
| */ | ||
| // GracefulShutdownCallback is the callback for graceful shutdown | ||
| // name: protocol name such as "grpc", "tri", "dubbo" | ||
| // returns error if notify failed | ||
| type GracefulShutdownCallback func(ctx context.Context) error | ||
|
|
||
| var ( | ||
| customShutdownCallbacks = list.New() | ||
| gracefulShutdownCallbacks = make(map[string]GracefulShutdownCallback) | ||
| ) | ||
|
|
||
| // AddCustomShutdownCallback adds custom shutdown callback | ||
| func AddCustomShutdownCallback(callback func()) { | ||
| customShutdownCallbacks.PushBack(callback) | ||
| } | ||
|
|
||
| // GetAllCustomShutdownCallbacks gets all custom shutdown callbacks | ||
| // GetAllCustomShutdownCallbacks returns all custom shutdown callbacks | ||
| func GetAllCustomShutdownCallbacks() *list.List { | ||
| return customShutdownCallbacks | ||
| } | ||
|
|
||
| // SetGracefulShutdownCallback sets protocol-level graceful shutdown callback | ||
| func SetGracefulShutdownCallback(name string, f GracefulShutdownCallback) { | ||
| gracefulShutdownCallbacks[name] = f | ||
| } | ||
|
|
||
| // GetGracefulShutdownCallback returns protocol's graceful shutdown callback | ||
| func GetGracefulShutdownCallback(name string) (GracefulShutdownCallback, bool) { | ||
| f, ok := gracefulShutdownCallbacks[name] | ||
| return f, ok | ||
| } | ||
|
|
||
| // GetAllGracefulShutdownCallbacks returns all protocol's graceful shutdown callbacks | ||
| func GetAllGracefulShutdownCallbacks() map[string]GracefulShutdownCallback { | ||
| return gracefulShutdownCallbacks | ||
|
Comment on lines
+46
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
建议:加
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I'll correct it |
||
| } | ||
|
Comment on lines
+45
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
func RegisterGracefulShutdownCallback(name string, f GracefulShutdownCallback)
func LookupGracefulShutdownCallback(name string) (GracefulShutdownCallback, bool)
func GracefulShutdownCallbacks() map[string]GracefulShutdownCallback
extension.SetGracefulShutdownCallback(GRPC, cb1)
extension.SetGracefulShutdownCallback(GRPC, cb2)如果不允许需要加一下判断
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,7 +19,11 @@ package graceful_shutdown | |||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "errors" | ||||||
| "strconv" | ||||||
| "strings" | ||||||
| "sync" | ||||||
| "time" | ||||||
| ) | ||||||
|
|
||||||
| import ( | ||||||
|
|
@@ -50,7 +54,8 @@ func init() { | |||||
| } | ||||||
|
|
||||||
| type consumerGracefulShutdownFilter struct { | ||||||
| shutdownConfig *global.ShutdownConfig | ||||||
| shutdownConfig *global.ShutdownConfig | ||||||
| closingInvokers sync.Map // map[string]time.Time (url key -> expire time) | ||||||
| } | ||||||
|
|
||||||
| func newConsumerGracefulShutdownFilter() filter.Filter { | ||||||
|
|
@@ -62,15 +67,31 @@ func newConsumerGracefulShutdownFilter() filter.Filter { | |||||
| return csf | ||||||
| } | ||||||
|
|
||||||
| // Invoke adds the requests count and block the new requests if application is closing | ||||||
| // Invoke adds the requests count and checks if invoker is closing | ||||||
| func (f *consumerGracefulShutdownFilter) Invoke(ctx context.Context, invoker base.Invoker, invocation base.Invocation) result.Result { | ||||||
| // check if invoker is closing | ||||||
| if f.isClosingInvoker(invoker) { | ||||||
| logger.Warnf("Graceful shutdown: skipping closing invoker: %s", invoker.GetURL().String()) | ||||||
| return &result.RPCResult{Err: errors.New("provider is closing")} | ||||||
| } | ||||||
| f.shutdownConfig.ConsumerActiveCount.Inc() | ||||||
| return invoker.Invoke(ctx, invocation) | ||||||
| } | ||||||
|
|
||||||
| // OnResponse reduces the number of active processes then return the process result | ||||||
| func (f *consumerGracefulShutdownFilter) OnResponse(ctx context.Context, result result.Result, invoker base.Invoker, invocation base.Invocation) result.Result { | ||||||
| f.shutdownConfig.ConsumerActiveCount.Dec() | ||||||
|
|
||||||
| // check closing flag in response | ||||||
| if f.isClosingResponse(result) { | ||||||
| f.markClosingInvoker(invoker) | ||||||
| } | ||||||
|
|
||||||
| // handle request error | ||||||
| if result.Error() != nil { | ||||||
| f.handleRequestError(invoker, result.Error()) | ||||||
| } | ||||||
|
|
||||||
| return result | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -91,3 +112,89 @@ func (f *consumerGracefulShutdownFilter) Set(name string, conf any) { | |||||
| // do nothing | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // isClosingInvoker checks if invoker is in closing list | ||||||
| func (f *consumerGracefulShutdownFilter) isClosingInvoker(invoker base.Invoker) bool { | ||||||
| key := invoker.GetURL().String() | ||||||
| if expireTime, ok := f.closingInvokers.Load(key); ok { | ||||||
| if time.Now().Before(expireTime.(time.Time)) { | ||||||
| return true | ||||||
| } | ||||||
| f.closingInvokers.Delete(key) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
建议: |
||||||
| } | ||||||
| return false | ||||||
| } | ||||||
|
|
||||||
| // isClosingResponse checks if response contains closing flag | ||||||
| func (f *consumerGracefulShutdownFilter) isClosingResponse(result result.Result) bool { | ||||||
| if result != nil && result.Attachments() != nil { | ||||||
| if v, ok := result.Attachments()[constant.GracefulShutdownClosingKey]; ok { | ||||||
| if v == "true" { | ||||||
| return true | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| return false | ||||||
| } | ||||||
|
|
||||||
| // markClosingInvoker marks invoker as closing and sets available=false | ||||||
| func (f *consumerGracefulShutdownFilter) markClosingInvoker(invoker base.Invoker) { | ||||||
| key := invoker.GetURL().String() | ||||||
| expireTime := time.Now().Add(f.getClosingInvokerExpireTime()) | ||||||
| f.closingInvokers.Store(key, expireTime) | ||||||
|
|
||||||
| logger.Infof("Graceful shutdown: marked invoker as closing: %s, will expire at %v, IsAvailable=%v", | ||||||
| key, expireTime, invoker.IsAvailable()) | ||||||
|
|
||||||
| if bi, ok := invoker.(*base.BaseInvoker); ok { | ||||||
| bi.SetAvailable(false) | ||||||
| logger.Infof("Graceful shutdown: set invoker unavailable: %s, IsAvailable now=%v", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| key, invoker.IsAvailable()) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func (f *consumerGracefulShutdownFilter) getClosingInvokerExpireTime() time.Duration { | ||||||
| if f.shutdownConfig != nil && f.shutdownConfig.ClosingInvokerExpireTime != "" { | ||||||
| if duration, err := time.ParseDuration(f.shutdownConfig.ClosingInvokerExpireTime); err == nil && duration > 0 { | ||||||
| return duration | ||||||
| } | ||||||
| } | ||||||
| // default 30s, also try parsing numeric string as milliseconds | ||||||
| if f.shutdownConfig != nil && f.shutdownConfig.ClosingInvokerExpireTime != "" { | ||||||
| if ms, err := strconv.ParseInt(f.shutdownConfig.ClosingInvokerExpireTime, 10, 64); err == nil && ms > 0 { | ||||||
| return time.Duration(ms) * time.Millisecond | ||||||
| } | ||||||
| } | ||||||
| return 30 * time.Second | ||||||
| } | ||||||
|
|
||||||
| // handleRequestError handles request errors and marks invoker as unavailable for connection errors | ||||||
| func (f *consumerGracefulShutdownFilter) handleRequestError(invoker base.Invoker, err error) { | ||||||
| if err == nil { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| // check for connection-related errors | ||||||
| errMsg := err.Error() | ||||||
| isConnectionError := strings.Contains(errMsg, "client has closed") || | ||||||
| strings.Contains(errMsg, "connection") || | ||||||
|
Comment on lines
+179
to
+180
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
建议:改用
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I'll correct it |
||||||
| strings.Contains(errMsg, "EOF") || | ||||||
| strings.Contains(errMsg, "broken pipe") || | ||||||
| strings.Contains(errMsg, "gRPC") && strings.Contains(errMsg, "closing") || | ||||||
| strings.Contains(errMsg, "http2") && strings.Contains(errMsg, "close") | ||||||
|
|
||||||
| if isConnectionError { | ||||||
| key := invoker.GetURL().String() | ||||||
| expireTime := time.Now().Add(f.getClosingInvokerExpireTime()) | ||||||
| f.closingInvokers.Store(key, expireTime) | ||||||
|
|
||||||
| logger.Infof("Graceful shutdown: connection error detected for invoker: %s, marking as closing, will expire at %v, IsAvailable=%v", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 其他的logger.Infof都改一下 |
||||||
| key, expireTime, invoker.IsAvailable()) | ||||||
|
|
||||||
| if bi, ok := invoker.(*base.BaseInvoker); ok { | ||||||
| bi.SetAvailable(false) | ||||||
| logger.Infof("Graceful shutdown: set invoker unavailable due to connection error: %s, IsAvailable now=%v", | ||||||
| key, invoker.IsAvailable()) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,6 @@ var ( | |
| ) | ||
|
|
||
| func init() { | ||
| // `init()` is performed before config.Load(), so shutdownConfig will be retrieved after config was loaded. | ||
| extension.SetFilter(constant.GracefulShutdownProviderFilterKey, func() filter.Filter { | ||
| return newProviderGracefulShutdownFilter() | ||
| }) | ||
|
|
@@ -85,6 +84,12 @@ func (f *providerGracefulShutdownFilter) Invoke(ctx context.Context, invoker bas | |
| // OnResponse reduces the number of active processes then return the process result | ||
| func (f *providerGracefulShutdownFilter) OnResponse(ctx context.Context, result result.Result, invoker base.Invoker, invocation base.Invocation) result.Result { | ||
| f.shutdownConfig.ProviderActiveCount.Dec() | ||
|
|
||
| // add closing flag to response | ||
| if f.isClosing() { | ||
| result.AddAttachment(constant.GracefulShutdownClosingKey, "true") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
建议:加 nil 检查: if f.isClosing() && result != nil {
result.AddAttachment(constant.GracefulShutdownClosingKey, "true")
} |
||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
|
|
@@ -94,15 +99,13 @@ func (f *providerGracefulShutdownFilter) Set(name string, conf any) { | |
| switch ct := conf.(type) { | ||
| case *global.ShutdownConfig: | ||
| f.shutdownConfig = ct | ||
| // only for compatibility with old config, able to directly remove after config is deleted | ||
| case *config.ShutdownConfig: | ||
| f.shutdownConfig = compatGlobalShutdownConfig(ct) | ||
| default: | ||
| logger.Warnf("the type of config for {%s} should be *global.ShutdownConfig", constant.GracefulShutdownFilterShutdownConfig) | ||
| } | ||
| return | ||
| default: | ||
| // do nothing | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -112,3 +115,10 @@ func (f *providerGracefulShutdownFilter) rejectNewRequest() bool { | |
| } | ||
| return f.shutdownConfig.RejectRequest.Load() | ||
| } | ||
|
|
||
| func (f *providerGracefulShutdownFilter) isClosing() bool { | ||
| if f.shutdownConfig == nil { | ||
| return false | ||
| } | ||
| return f.shutdownConfig.Closing.Load() | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个注释也别删