From 7aa4218ac2d59a80954225163d9bf565e4c69ad8 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Fri, 8 Nov 2019 23:02:14 +0800 Subject: [PATCH 01/13] Final version --- common/extension/graceful_shutdown.go | 55 ++++++++ config/config_loader.go | 1 + config/consumer_config.go | 1 + config/graceful_shutdown.go | 184 ++++++++++++++++++++++++++ config/graceful_shutdown_config.go | 26 ++++ config/provider_config.go | 12 +- go.sum | 4 - protocol/rpc_status.go | 14 ++ 8 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 common/extension/graceful_shutdown.go create mode 100644 config/graceful_shutdown.go create mode 100644 config/graceful_shutdown_config.go diff --git a/common/extension/graceful_shutdown.go b/common/extension/graceful_shutdown.go new file mode 100644 index 0000000000..ff34f5073e --- /dev/null +++ b/common/extension/graceful_shutdown.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package extension + +import ( + "container/list" +) + +var ( + // SystemShutdownCallbackNames = []string{"registry"} + // systemShutdownCallbacks = make(map[string]func()) + customShutdownCallbacks = list.New() +) + +/** + * 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. + */ +func AddCustomShutdownCallback(callback func()) { + customShutdownCallbacks.PushBack(callback) +} + +func GetAllCustomShutdownCallbacks() *list.List { + return customShutdownCallbacks +} diff --git a/config/config_loader.go b/config/config_loader.go index b737d3f233..2747a83652 100644 --- a/config/config_loader.go +++ b/config/config_loader.go @@ -149,6 +149,7 @@ func Load() { } } } + GracefulShutdownInit() } // get rpc service for consumer diff --git a/config/consumer_config.go b/config/consumer_config.go index b1ebdd5d8e..54b87e22f4 100644 --- a/config/consumer_config.go +++ b/config/consumer_config.go @@ -57,6 +57,7 @@ type ConsumerConfig struct { References map[string]*ReferenceConfig `yaml:"references" json:"references,omitempty" property:"references"` ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf"` FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` } func (c *ConsumerConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go new file mode 100644 index 0000000000..cb198c1255 --- /dev/null +++ b/config/graceful_shutdown.go @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" + + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/protocol" +) + +/* + * The key point is that find out the signals to handle. + * The most important documentation is https://golang.org/pkg/os/signal/ + * From this documentation, we can know that: + * 1. The signals SIGKILL and SIGSTOP may not be caught by signal package; + * 2. SIGHUP, SIGINT, or SIGTERM signal causes the program to exit + * 3. SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, or SIGSYS signal causes the program to exit with a stack dump + * 4. The invocation of Notify(signal...) will disable the default behavior of those signals. + * + * So the signals SIGKILL, SIGSTOP, SIGHUP, SIGINT, SIGTERM, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, SIGSYS + * should be processed. + * It's seems that the Unix/Linux does not have the signal SIGSTKFLT. https://github.com/golang/go/issues/33381 + * So this signal will be ignored. + * + */ +var gracefulShutdownOnce = sync.Once{} + +func GracefulShutdownInit() { + + signals := make(chan os.Signal, 1) + + signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGKILL, syscall.SIGSTOP, + syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, + syscall.SIGABRT, syscall.SIGEMT, syscall.SIGSYS, + ) + for { + sig := <-signals + + gracefulShutdownOnce.Do(func() { + logger.Infof("get signal %s, application is shutdown", sig.String()) + + destroyAllRegistries() + // waiting for a short time so that the clients have enough time to get the notification that server shutdowns + // The value of configuration depends on how long the clients will get notification. + waitAndAcceptNewRequests() + + // after this step, the new request will be rejected because the server is shutdown. + destroyProviderProtocols() + + // + + + logger.Infof("Execute the custom callbacks.") + customCallbacks := extension.GetAllCustomShutdownCallbacks() + for callback := customCallbacks.Front(); callback != nil; callback = callback.Next() { + callback.Value.(func())() + } + }) + } +} + +func destroyAllRegistries() { + registryProtocol := extension.GetProtocol(constant.REGISTRY_KEY) + registryProtocol.Destroy() +} + +func destroyConsumerProtocols() { + if consumerConfig == nil || consumerConfig.ProtocolConf == nil { + return + } + destroyProtocols(consumerConfig.ProtocolConf) +} +func destroyProviderProtocols() { + if providerConfig == nil || providerConfig.ProtocolConf == nil { + return + } + destroyProtocols(providerConfig.ProtocolConf) +} + +func destroyProtocols(protocolConf interface{}) { + protocols := protocolConf.(map[interface{}]interface{}) + for name, _ := range protocols { + protocol := extension.GetProtocol(name.(string)) + protocol.Destroy() + } +} + +func waitAndAcceptNewRequests() { + + if providerConfig == nil || providerConfig.ShutdownConfig == nil { + return + } + shutdownConfig := providerConfig.ShutdownConfig + + timeout, err := strconv.ParseInt(shutdownConfig.AcceptNewRequestsTimeout, 0, 0) + if err != nil { + logger.Errorf("The timeout configuration of keeping accept new requests is invalid. Go next step!", err) + return + } + + // ignore this phase + if timeout < 0 { + return + } + + var duration = time.Duration(timeout) * time.Millisecond + + time.Sleep(duration) +} + +/** + * this method will wait a short time until timeout or all requests have been processed. + * this implementation use the active filter, so you need to add the filter into your application configuration + * for example: + * server.yml or client.yml + * + * filter: "active", + * + * see the ActiveFilter for more detail. + * We use the bigger value between consumer's config and provider's config + * if the application is both consumer and provider. + * This method's behavior is a little bit complicated. + */ +func waitForProcessingRequest() { + var timeout int64 = 0 + if providerConfig != nil && providerConfig.ShutdownConfig != nil { + timeout = waitingProcessedTimeout(providerConfig.ShutdownConfig) + } + + if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { + consumerTimeout := waitingProcessedTimeout(consumerConfig.ShutdownConfig) + if consumerTimeout > timeout { + timeout = consumerTimeout + } + } + if timeout <= 0{ + return + } + + timeout = timeout * time.Millisecond.Nanoseconds() + + start := time.Now().UnixNano() + + for time.Now().UnixNano() - start < timeout && protocol.GetTotalActive() > 0 { + // sleep 10 ms and then we check it again + time.Sleep(10 * time.Millisecond) + } +} + +func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) int64 { + if len(shutdownConfig.WaitingProcessRequestsTimeout) <=0 { + return 0 + } + config, err := strconv.ParseInt(shutdownConfig.WaitingProcessRequestsTimeout, 0, 0) + if err != nil { + logger.Errorf("The configuration of shutdownConfig.WaitingProcessRequestsTimeout is invalid: %s", + shutdownConfig.WaitingProcessRequestsTimeout) + return 0 + } + return config +} diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go new file mode 100644 index 0000000000..9c52044e35 --- /dev/null +++ b/config/graceful_shutdown_config.go @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + + +type ShutdownConfig struct { + // it's the total timeout configuration. After that, the application will exit. + Timeout string `yaml:"timeout" json:"timeout,omitempty" property:"timout"` + AcceptNewRequestsTimeout string `yaml:"requests.accept.timeout" json:"requests.accept.timeout,omitempty" property:"requests.accept.timeout"` + WaitingProcessRequestsTimeout string `yaml:"requests.reject.timeout" json:"requests.reject.timeout,omitempty" property:"requests.reject.timeout"` +} diff --git a/config/provider_config.go b/config/provider_config.go index 00faa1d0ab..d1de691be7 100644 --- a/config/provider_config.go +++ b/config/provider_config.go @@ -44,11 +44,12 @@ type ProviderConfig struct { ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` - Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` - Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` - Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` - ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` - FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` + Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` + Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` + ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` + FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` } func (c *ProviderConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -106,6 +107,7 @@ func ProviderInit(confProFile string) error { } logger.Debugf("provider config{%#v}\n", providerConfig) + return nil } diff --git a/go.sum b/go.sum index bcde5b1f80..b730b78684 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190802083043-4cd0c391755e/go.mod github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/apache/dubbo-go-hessian2 v1.2.5-0.20191029001541-894e45c9aaaa h1:11TO1wiM5bvGAVrmfN5atD8gZqUSPE1TBoIs8sI6Abk= github.com/apache/dubbo-go-hessian2 v1.2.5-0.20191029001541-894e45c9aaaa/go.mod h1:LWnndnrFXZmJLAzoyNAPNHSIJ1KOHVkTSsHgC3YYWlo= -github.com/apache/dubbo-go-hessian2 v1.3.0 h1:ZhQYDm8GHqIp6i53T4ZJHQBN11nAYAjxlwoVznfyvD8= -github.com/apache/dubbo-go-hessian2 v1.3.0/go.mod h1:LWnndnrFXZmJLAzoyNAPNHSIJ1KOHVkTSsHgC3YYWlo= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -104,8 +102,6 @@ github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dubbogo/getty v1.3.0 h1:GImOCANdts7dlRqi9GMVsZJnfst9EPyjTVTR1AesOD8= -github.com/dubbogo/getty v1.3.0/go.mod h1:K4b3MkGLf7T+lMgQNFgpg0dI1Wvv1PTisFs1Psf86kU= github.com/dubbogo/getty v1.3.1 h1:9fehwTo/D6+z6/+kADMbhbKeMkP80o/3g+XwV5lFLTY= github.com/dubbogo/getty v1.3.1/go.mod h1:dtLOEb1v6EMHsQNYRWEACiRLmTWB2kJGUAj1aXayPOg= github.com/dubbogo/gost v1.1.1 h1:JCM7vx5edPIjDA5ovJTuzEEXuw2t7xLyrlgi2mi5jHI= diff --git a/protocol/rpc_status.go b/protocol/rpc_status.go index 3a8bfbc87f..4f7e63f9f0 100644 --- a/protocol/rpc_status.go +++ b/protocol/rpc_status.go @@ -73,3 +73,17 @@ func beginCount0(rpcStatus *RpcStatus) { func endCount0(rpcStatus *RpcStatus) { atomic.AddInt32(&rpcStatus.active, -1) } + +func GetTotalActive() int32 { + var result int32 = 0 + methodStatistics.Range(func(_, value interface{}) bool { + statics := value.(*sync.Map) + statics.Range(func(_, value interface{}) bool { + rpcStatus := value.(*RpcStatus) + result = result + rpcStatus.active + return true + }) + return true + }) + return result +} From deec46e47e57ead84b4df9ff40ce9df12ef4a283 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Sun, 10 Nov 2019 21:39:45 +0800 Subject: [PATCH 02/13] Fix BUG & DEBUG --- common/constant/key.go | 2 + config/config_loader.go | 3 +- config/graceful_shutdown.go | 168 +++++++++++++++----------- config/graceful_shutdown_config.go | 57 ++++++++- config/graceful_shutdown_test.go | 56 +++++++++ filter/impl/GracefulShutdownFilter.go | 84 +++++++++++++ registry/zookeeper/listener.go | 10 +- registry/zookeeper/registry.go | 11 +- 8 files changed, 302 insertions(+), 89 deletions(-) create mode 100644 config/graceful_shutdown_test.go create mode 100644 filter/impl/GracefulShutdownFilter.go diff --git a/common/constant/key.go b/common/constant/key.go index ff371d08c6..2bfebf8f07 100644 --- a/common/constant/key.go +++ b/common/constant/key.go @@ -71,6 +71,8 @@ const ( EXECUTE_LIMIT_KEY = "execute.limit" DEFAULT_EXECUTE_LIMIT = "-1" EXECUTE_REJECTED_EXECUTION_HANDLER_KEY = "execute.limit.rejected.handler" + PROVIDER_SHUTDOWN_FILTER = "pshutdown" + CONSUMER_SHUTDOWN_FILTER = "cshutdown" ) const ( diff --git a/config/config_loader.go b/config/config_loader.go index 2747a83652..106d10ede4 100644 --- a/config/config_loader.go +++ b/config/config_loader.go @@ -149,7 +149,8 @@ func Load() { } } } - GracefulShutdownInit() + // init the shutdown callback + // GracefulShutdownInit() } // get rpc service for consumer diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index cb198c1255..8787cb5ac6 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -20,7 +20,7 @@ package config import ( "os" "os/signal" - "strconv" + "runtime/debug" "sync" "syscall" "time" @@ -28,7 +28,6 @@ import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" - "github.com/apache/dubbo-go/protocol" ) /* @@ -56,44 +55,96 @@ func GracefulShutdownInit() { syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, syscall.SIGABRT, syscall.SIGEMT, syscall.SIGSYS, ) - for { - sig := <-signals - gracefulShutdownOnce.Do(func() { - logger.Infof("get signal %s, application is shutdown", sig.String()) + go func() { + select { + case sig := <-signals: + logger.Infof("get signal %s, application will shutdown.", sig.String()) + // gracefulShutdownOnce.Do(func() { + BeforeShutdown() + + switch sig { + // those signals' original behavior is exit with dump ths stack, so we try to keep the behavior + case syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, + syscall.SIGABRT, syscall.SIGEMT, syscall.SIGSYS: + debug.WriteHeapDump(os.Stdout.Fd()) + default: + time.AfterFunc(totalTimeout(), func() { + logger.Warn("Shutdown gracefully timeout, application will shutdown immediately. ") + os.Exit(0) + }) + } + os.Exit(0) + } + }() +} - destroyAllRegistries() - // waiting for a short time so that the clients have enough time to get the notification that server shutdowns - // The value of configuration depends on how long the clients will get notification. - waitAndAcceptNewRequests() +func totalTimeout() time.Duration { + var providerShutdown time.Duration = 0 + if providerConfig != nil && providerConfig.ShutdownConfig != nil { + providerShutdown = providerConfig.ShutdownConfig.GetTimeout() + } - // after this step, the new request will be rejected because the server is shutdown. - destroyProviderProtocols() + var consumerShutdown time.Duration = 0 + if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { + consumerShutdown = consumerConfig.ShutdownConfig.GetTimeout() + } - // + var timeout = providerShutdown + if consumerShutdown > providerShutdown { + timeout = consumerShutdown + } + return timeout +} +func BeforeShutdown() { - logger.Infof("Execute the custom callbacks.") - customCallbacks := extension.GetAllCustomShutdownCallbacks() - for callback := customCallbacks.Front(); callback != nil; callback = callback.Next() { - callback.Value.(func())() - } - }) + destroyAllRegistries() + // waiting for a short time so that the clients have enough time to get the notification that server shutdowns + // The value of configuration depends on how long the clients will get notification. + waitAndAcceptNewRequests() + + // reject the new request, but keeping waiting for accepting requests + waitForReceivingRequests() + + // If this application is not the provider, it will do nothing + destroyProviderProtocols() + + // waiting for accepted requests to be processed. + + // after this step, the response from other providers will be rejected. + // If this application is not the consumer, it will do nothing + destroyConsumerProtocols() + + logger.Infof("Execute the custom callbacks.") + customCallbacks := extension.GetAllCustomShutdownCallbacks() + for callback := customCallbacks.Front(); callback != nil; callback = callback.Next() { + callback.Value.(func())() } } func destroyAllRegistries() { + logger.Infof("Graceful shutdown --- Destroy all registries. ") registryProtocol := extension.GetProtocol(constant.REGISTRY_KEY) registryProtocol.Destroy() } func destroyConsumerProtocols() { + logger.Info("Graceful shutdown --- Destroy consumer's protocols. ") if consumerConfig == nil || consumerConfig.ProtocolConf == nil { return } destroyProtocols(consumerConfig.ProtocolConf) } + +/** + * destroy the provider's protocol. + * if the protocol is consumer's protocol too, we will keep it. + */ func destroyProviderProtocols() { + + logger.Info("Graceful shutdown --- Destroy provider's protocols. ") + if providerConfig == nil || providerConfig.ProtocolConf == nil { return } @@ -103,82 +154,53 @@ func destroyProviderProtocols() { func destroyProtocols(protocolConf interface{}) { protocols := protocolConf.(map[interface{}]interface{}) for name, _ := range protocols { - protocol := extension.GetProtocol(name.(string)) - protocol.Destroy() + extension.GetProtocol(name.(string)).Destroy() } } func waitAndAcceptNewRequests() { + logger.Info("Graceful shutdown --- Keep waiting and accept new requests for a short time. ") if providerConfig == nil || providerConfig.ShutdownConfig == nil { return } - shutdownConfig := providerConfig.ShutdownConfig - timeout, err := strconv.ParseInt(shutdownConfig.AcceptNewRequestsTimeout, 0, 0) - if err != nil { - logger.Errorf("The timeout configuration of keeping accept new requests is invalid. Go next step!", err) - return - } + timeout := providerConfig.ShutdownConfig.GetStepTimeout() - // ignore this phase + // ignore this step if timeout < 0 { return } - - var duration = time.Duration(timeout) * time.Millisecond - - time.Sleep(duration) + time.Sleep(timeout) } -/** - * this method will wait a short time until timeout or all requests have been processed. - * this implementation use the active filter, so you need to add the filter into your application configuration - * for example: - * server.yml or client.yml - * - * filter: "active", - * - * see the ActiveFilter for more detail. - * We use the bigger value between consumer's config and provider's config - * if the application is both consumer and provider. - * This method's behavior is a little bit complicated. - */ -func waitForProcessingRequest() { - var timeout int64 = 0 - if providerConfig != nil && providerConfig.ShutdownConfig != nil { - timeout = waitingProcessedTimeout(providerConfig.ShutdownConfig) +// for provider. It will wait for processing receiving requests +func waitForReceivingRequests() { + logger.Info("Graceful shutdown --- Keep waiting until accepting requests finish or timeout. ") + if providerConfig == nil || providerConfig.ShutdownConfig == nil { + // ignore this step + return } + waitingProcessedTimeout(providerConfig.ShutdownConfig) +} - if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { - consumerTimeout := waitingProcessedTimeout(consumerConfig.ShutdownConfig) - if consumerTimeout > timeout { - timeout = consumerTimeout - } - } - if timeout <= 0{ +// for consumer. It will wait for the response of sending requests +func waitForSendingRequests() { + logger.Info("Graceful shutdown --- Keep waiting until sending requests getting response or timeout ") + if consumerConfig == nil || consumerConfig.ShutdownConfig == nil { + // ignore this step return } +} - timeout = timeout * time.Millisecond.Nanoseconds() - +func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) { + timeout := shutdownConfig.GetStepTimeout() + if timeout <= 0 { + return + } start := time.Now().UnixNano() - - for time.Now().UnixNano() - start < timeout && protocol.GetTotalActive() > 0 { + for time.Now().UnixNano()-start < timeout.Nanoseconds() && !shutdownConfig.RequestsFinished { // sleep 10 ms and then we check it again time.Sleep(10 * time.Millisecond) } } - -func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) int64 { - if len(shutdownConfig.WaitingProcessRequestsTimeout) <=0 { - return 0 - } - config, err := strconv.ParseInt(shutdownConfig.WaitingProcessRequestsTimeout, 0, 0) - if err != nil { - logger.Errorf("The configuration of shutdownConfig.WaitingProcessRequestsTimeout is invalid: %s", - shutdownConfig.WaitingProcessRequestsTimeout) - return 0 - } - return config -} diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go index 9c52044e35..c5c16add05 100644 --- a/config/graceful_shutdown_config.go +++ b/config/graceful_shutdown_config.go @@ -17,10 +17,59 @@ package config +import ( + "strconv" + "time" + "github.com/apache/dubbo-go/common/logger" +) + +const ( + defaultTimeout = 60 * time.Second + defaultStepTimeout = 10 * time.Second +) type ShutdownConfig struct { - // it's the total timeout configuration. After that, the application will exit. - Timeout string `yaml:"timeout" json:"timeout,omitempty" property:"timout"` - AcceptNewRequestsTimeout string `yaml:"requests.accept.timeout" json:"requests.accept.timeout,omitempty" property:"requests.accept.timeout"` - WaitingProcessRequestsTimeout string `yaml:"requests.reject.timeout" json:"requests.reject.timeout,omitempty" property:"requests.reject.timeout"` + /* + * Total timeout. Even though we don't release all resources, + * the application will shutdown if the costing time is over this configuration. The unit is ms. + * default value is 60 * 1000 ms = 1 minutes + * In general, it should be bigger than 3 * StepTimeout. + */ + Timeout string `yaml:"timeout" json:"timeout,omitempty" property:"timeout"` + /* + * the timeout on each step. You should evaluate the response time of request + * and the time that client noticed that server shutdown. + * For example, if your client will received the notification within 10s when you start to close server, + * and the 99.9% requests will return response in 2s, so the StepTimeout will be bigger than(10+2) * 1000ms, + * maybe (10 + 2*3) * 1000ms is a good choice. + */ + StepTimeout string `yaml:"step_timeout" json:"step.timeout,omitempty" property:"step.timeout"` + // when we try to shutdown the application, we will reject the new requests. In most cases, you don't need to configure this. + RejectRequestHandler string `yaml:"reject_handler" json:"reject_handler,omitempty" property:"reject_handler"` + // true -> new request will be rejected. + RejectRequest bool + + // true -> all requests had been processed. In provider side it means that all requests are returned response to clients + // In consumer side, it means that all requests getting response from servers + RequestsFinished bool +} + +func (config *ShutdownConfig) GetTimeout() time.Duration { + result, err := strconv.ParseInt(config.Timeout, 0, 0) + if err != nil { + logger.Errorf("The Timeout configuration is invalid: %s, and we will use the default value: %s", + config.Timeout, defaultTimeout.String(), err) + return defaultTimeout + } + return time.Millisecond * time.Duration(result) +} + +func (config *ShutdownConfig) GetStepTimeout() time.Duration { + result, err := strconv.ParseInt(config.StepTimeout, 0, 0) + if err != nil { + logger.Errorf("The StepTimeout configuration is invalid: %s, and we will use the default value: %s", + config.Timeout, defaultStepTimeout.String(), err) + return defaultStepTimeout + } + return time.Millisecond * time.Duration(result) } diff --git a/config/graceful_shutdown_test.go b/config/graceful_shutdown_test.go new file mode 100644 index 0000000000..5958562089 --- /dev/null +++ b/config/graceful_shutdown_test.go @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "testing" + + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/protocol" +) + + +func TestBeforeShutdown(t *testing.T) { + extension.SetProtocol("registry", func() protocol.Protocol { + return &mockRegistryProtocol{} + }) + extension.SetProtocol(constant.DUBBO, func() protocol.Protocol { + return &mockRegistryProtocol{} + }) + + protocolConfigs := make(map[interface{}]interface{}) + protocolConfigs[constant.DUBBO] = "aaa" + providerConfig = &ProviderConfig{ + ShutdownConfig: &ShutdownConfig{ + Timeout: "1", + AcceptNewRequestsTimeout: "1", + WaitingProcessRequestsTimeout: "1", + }, + ProtocolConf: protocolConfigs, + } + consumerConfig = &ConsumerConfig{ + ProtocolConf: protocolConfigs, + ShutdownConfig: &ShutdownConfig{ + Timeout: "1", + AcceptNewRequestsTimeout: "1", + WaitingProcessRequestsTimeout: "1", + }, + } + BeforeShutdown() +} \ No newline at end of file diff --git a/filter/impl/GracefulShutdownFilter.go b/filter/impl/GracefulShutdownFilter.go new file mode 100644 index 0000000000..7337b20d0d --- /dev/null +++ b/filter/impl/GracefulShutdownFilter.go @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package impl + +import ( + "sync/atomic" + + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/common/logger" + "github.com/apache/dubbo-go/config" + "github.com/apache/dubbo-go/filter" + "github.com/apache/dubbo-go/filter/common" + "github.com/apache/dubbo-go/protocol" +) + +func init() { + var consumerFiler = &gracefulShutdownFilter{ + activeCount: 0, + shutdownConfig: config.GetConsumerConfig().ShutdownConfig, + } + var providerFilter = &gracefulShutdownFilter{activeCount: 0} + + extension.SetFilter(constant.CONSUMER_SHUTDOWN_FILTER, func() filter.Filter { + return consumerFiler + }) + + extension.SetFilter(constant.PROVIDER_SHUTDOWN_FILTER, func() filter.Filter { + return providerFilter + }) +} + +type gracefulShutdownFilter struct { + activeCount int32 + shutdownConfig *config.ShutdownConfig +} + +func (gf *gracefulShutdownFilter) Invoke(invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { + if gf.rejectNewRequest() { + logger.Info("The application is closing, new request will be rejected.") + return gf.getRejectHandler().RejectedExecution(invoker.GetUrl(), invocation) + } + atomic.AddInt32(&gf.activeCount, 1) + return invoker.Invoke(invocation) +} + +func (gf *gracefulShutdownFilter) OnResponse(result protocol.Result, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { + atomic.AddInt32(&gf.activeCount, -1) + // although this isn't thread safe, it won't be a problem if the gf.rejectNewRequest() is true. + if gf.activeCount <= 0 { + gf.shutdownConfig.RequestsFinished = true + } + return result +} + +func (gf *gracefulShutdownFilter) rejectNewRequest() bool { + if gf.shutdownConfig == nil { + return false + } + return gf.shutdownConfig.RejectRequest +} + +func (gf *gracefulShutdownFilter) getRejectHandler() common.RejectedExecutionHandler { + handler := constant.DEFAULT_KEY + if gf.shutdownConfig != nil && len(gf.shutdownConfig.RejectRequestHandler) > 0 { + handler = gf.shutdownConfig.RejectRequestHandler + } + return extension.GetRejectedExecutionHandler(handler) +} diff --git a/registry/zookeeper/listener.go b/registry/zookeeper/listener.go index c25028d58f..67479c4436 100644 --- a/registry/zookeeper/listener.go +++ b/registry/zookeeper/listener.go @@ -92,7 +92,7 @@ func (l *RegistryConfigurationListener) Next() (*registry.ServiceEvent, error) { return nil, perrors.New("listener stopped") case <-l.registry.done: - logger.Warnf("zk consumer register has quit, so zk event listener exit asap now.") + logger.Warnf("zk consumer register has quit, so zk event listener exit now.") return nil, perrors.New("listener stopped") case e := <-l.events: @@ -109,13 +109,7 @@ func (l *RegistryConfigurationListener) Next() (*registry.ServiceEvent, error) { } } func (l *RegistryConfigurationListener) Close() { - if l.registry.IsAvailable() { - /** - * if the registry is not available, it means that the registry has been destroy - * so we don't need to call Done(), or it will cause the negative count panic for registry.wg - */ - l.registry.wg.Done() - } + l.registry.wg.Done() } func (l *RegistryConfigurationListener) valid() bool { diff --git a/registry/zookeeper/registry.go b/registry/zookeeper/registry.go index 29ae51d44f..682b9fe0cf 100644 --- a/registry/zookeeper/registry.go +++ b/registry/zookeeper/registry.go @@ -173,9 +173,14 @@ func (r *zkRegistry) GetUrl() common.URL { } func (r *zkRegistry) Destroy() { - if r.configListener != nil { - r.configListener.Close() - } + /** + * Don't r.listener.Close() + * here we don't close the listener because + * the listener will be close in Subscribe(). + * We can not close it here. If we do that, + * a negative count error of r.wg will occur because + * we close the listener twice. + */ close(r.done) r.wg.Wait() r.closeRegisters() From 9e9fc3ce390e319ed10194f24d9fd99a5809f9ac Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Mon, 11 Nov 2019 21:56:08 +0800 Subject: [PATCH 03/13] Fix BUG --- config/config_loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config_loader.go b/config/config_loader.go index 106d10ede4..414bb47902 100644 --- a/config/config_loader.go +++ b/config/config_loader.go @@ -150,7 +150,7 @@ func Load() { } } // init the shutdown callback - // GracefulShutdownInit() + GracefulShutdownInit() } // get rpc service for consumer From 634aeac4d5b328d5e7c3794c356c818faaa30923 Mon Sep 17 00:00:00 2001 From: "vito.he" Date: Wed, 30 Oct 2019 21:47:56 +0800 Subject: [PATCH 04/13] Add:File License --- before_ut.bat | 16 ++++++++++++++++ before_ut.sh | 17 +++++++++++++++++ config/provider_config_test.go | 17 +++++++++++++++++ dubbogo.png | Bin 58085 -> 0 bytes protocol/mock/mock_invoker.go | 16 ++++++++++++++++ registry/etcdv3/listener.go | 17 +++++++++++++++++ registry/etcdv3/listener_test.go | 17 +++++++++++++++++ registry/etcdv3/registry.go | 17 +++++++++++++++++ registry/etcdv3/registry_test.go | 17 +++++++++++++++++ registry/nacos/listener.go | 17 +++++++++++++++++ registry/nacos/registry.go | 17 +++++++++++++++++ registry/nacos/registry_test.go | 17 +++++++++++++++++ remoting/etcdv3/client.go | 17 +++++++++++++++++ remoting/etcdv3/client_test.go | 17 +++++++++++++++++ remoting/etcdv3/facade.go | 17 +++++++++++++++++ remoting/etcdv3/listener.go | 17 +++++++++++++++++ remoting/etcdv3/listener_test.go | 17 +++++++++++++++++ 17 files changed, 270 insertions(+) delete mode 100644 dubbogo.png diff --git a/before_ut.bat b/before_ut.bat index a10df71a7e..5296d0f876 100644 --- a/before_ut.bat +++ b/before_ut.bat @@ -1,3 +1,19 @@ +:: +:: Licensed to the Apache Software Foundation (ASF) under one or more +:: contributor license agreements. See the NOTICE file distributed with +:: this work for additional information regarding copyright ownership. +:: The ASF licenses this file to You under the Apache License, Version 2.0 +:: (the "License"); you may not use this file except in compliance with +:: the License. You may obtain a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 +:: +:: Unless required by applicable law or agreed to in writing, software +:: distributed under the License is distributed on an "AS IS" BASIS, +:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +:: See the License for the specific language governing permissions and +:: limitations under the License. + set zkJar=zookeeper-3.4.9-fatjar.jar md remoting\zookeeper\zookeeper-4unittest\contrib\fatjar config_center\zookeeper\zookeeper-4unittest\contrib\fatjar registry\zookeeper\zookeeper-4unittest\contrib\fatjar curl -L https://github.com/dubbogo/resources/raw/master/zookeeper-4unitest/contrib/fatjar/%zkJar% -o remoting/zookeeper/zookeeper-4unittest/contrib/fatjar/%zkJar% diff --git a/before_ut.sh b/before_ut.sh index 7acee76ce5..323173bcc6 100644 --- a/before_ut.sh +++ b/before_ut.sh @@ -1,3 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + mkdir -p remoting/zookeeper/zookeeper-4unittest/contrib/fatjar config_center/zookeeper/zookeeper-4unittest/contrib/fatjar registry/zookeeper/zookeeper-4unittest/contrib/fatjar wget -P "remoting/zookeeper/zookeeper-4unittest/contrib/fatjar" https://github.com/dubbogo/resources/raw/master/zookeeper-4unitest/contrib/fatjar/zookeeper-3.4.9-fatjar.jar cp remoting/zookeeper/zookeeper-4unittest/contrib/fatjar/zookeeper-3.4.9-fatjar.jar config_center/zookeeper/zookeeper-4unittest/contrib/fatjar/ diff --git a/config/provider_config_test.go b/config/provider_config_test.go index db4b5f9906..e8a9c1f7a7 100644 --- a/config/provider_config_test.go +++ b/config/provider_config_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package config import ( diff --git a/dubbogo.png b/dubbogo.png deleted file mode 100644 index 2cac434091276df102c3ae405c09621b8d8926ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58085 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 AKmY&$ diff --git a/protocol/mock/mock_invoker.go b/protocol/mock/mock_invoker.go index 557dafa277..c509cef054 100644 --- a/protocol/mock/mock_invoker.go +++ b/protocol/mock/mock_invoker.go @@ -1,3 +1,19 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + // Code generated by MockGen. DO NOT EDIT. // Source: invoker.go diff --git a/registry/etcdv3/listener.go b/registry/etcdv3/listener.go index e0dc099085..31d62fa916 100644 --- a/registry/etcdv3/listener.go +++ b/registry/etcdv3/listener.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/registry/etcdv3/listener_test.go b/registry/etcdv3/listener_test.go index 00024d2894..c064f99c6c 100644 --- a/registry/etcdv3/listener_test.go +++ b/registry/etcdv3/listener_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/registry/etcdv3/registry.go b/registry/etcdv3/registry.go index 4ee90969e5..b058113c69 100644 --- a/registry/etcdv3/registry.go +++ b/registry/etcdv3/registry.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/registry/etcdv3/registry_test.go b/registry/etcdv3/registry_test.go index 6da9ad9d7d..3f8c0f4cfc 100644 --- a/registry/etcdv3/registry_test.go +++ b/registry/etcdv3/registry_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/registry/nacos/listener.go b/registry/nacos/listener.go index 1f264ec9e4..25cd3d09b5 100644 --- a/registry/nacos/listener.go +++ b/registry/nacos/listener.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package nacos import ( diff --git a/registry/nacos/registry.go b/registry/nacos/registry.go index 810a1cb05f..a8b9fa83fa 100644 --- a/registry/nacos/registry.go +++ b/registry/nacos/registry.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package nacos import ( diff --git a/registry/nacos/registry_test.go b/registry/nacos/registry_test.go index 023ff78809..e6ab693cd3 100644 --- a/registry/nacos/registry_test.go +++ b/registry/nacos/registry_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package nacos import ( diff --git a/remoting/etcdv3/client.go b/remoting/etcdv3/client.go index 57d1211fe3..0509685653 100644 --- a/remoting/etcdv3/client.go +++ b/remoting/etcdv3/client.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/remoting/etcdv3/client_test.go b/remoting/etcdv3/client_test.go index 187789e0ab..8f9b80cd30 100644 --- a/remoting/etcdv3/client_test.go +++ b/remoting/etcdv3/client_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/remoting/etcdv3/facade.go b/remoting/etcdv3/facade.go index e75b39d6bc..499044b8d7 100644 --- a/remoting/etcdv3/facade.go +++ b/remoting/etcdv3/facade.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/remoting/etcdv3/listener.go b/remoting/etcdv3/listener.go index f5401917e2..a4d5805a6d 100644 --- a/remoting/etcdv3/listener.go +++ b/remoting/etcdv3/listener.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( diff --git a/remoting/etcdv3/listener_test.go b/remoting/etcdv3/listener_test.go index 33904a2134..7da8581973 100644 --- a/remoting/etcdv3/listener_test.go +++ b/remoting/etcdv3/listener_test.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package etcdv3 import ( From b5dfb5ef6cfd5abd4a367dfbcf20c1a7276d3c4d Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 14 Nov 2019 21:49:49 +0800 Subject: [PATCH 05/13] Adding GracefulShutdownFilter --- config/graceful_shutdown.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index 8787cb5ac6..8cf4083eb5 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -110,9 +110,9 @@ func BeforeShutdown() { // If this application is not the provider, it will do nothing destroyProviderProtocols() - // waiting for accepted requests to be processed. + // reject sending the new request, and waiting for response of sending requests + waitForSendingRequests() - // after this step, the response from other providers will be rejected. // If this application is not the consumer, it will do nothing destroyConsumerProtocols() @@ -134,7 +134,10 @@ func destroyConsumerProtocols() { if consumerConfig == nil || consumerConfig.ProtocolConf == nil { return } - destroyProtocols(consumerConfig.ProtocolConf) + protocols := consumerConfig.ProtocolConf.(map[interface{}]interface{}) + for name, _ := range protocols { + extension.GetProtocol(name.(string)).Destroy() + } } /** @@ -148,13 +151,21 @@ func destroyProviderProtocols() { if providerConfig == nil || providerConfig.ProtocolConf == nil { return } - destroyProtocols(providerConfig.ProtocolConf) -} -func destroyProtocols(protocolConf interface{}) { - protocols := protocolConf.(map[interface{}]interface{}) + consumerProtocol := make(map[string]interface{}, 0) + if consumerConfig != nil && consumerConfig.ProtocolConf != nil { + consumerProtocol = consumerConfig.ProtocolConf.(map[string]interface{}) + } + + protocols := providerConfig.ProtocolConf.(map[string]interface{}) for name, _ := range protocols { - extension.GetProtocol(name.(string)).Destroy() + _, found := consumerProtocol[name] + + // the protocol is the consumer's protocol, we can not destroy it. + if found { + continue + } + extension.GetProtocol(name).Destroy() } } @@ -185,7 +196,7 @@ func waitForReceivingRequests() { } // for consumer. It will wait for the response of sending requests -func waitForSendingRequests() { +func waitForSendingRequests() { logger.Info("Graceful shutdown --- Keep waiting until sending requests getting response or timeout ") if consumerConfig == nil || consumerConfig.ShutdownConfig == nil { // ignore this step From df91a1c45b9abf39ef72bb56c1983b42a55d497d Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 14 Nov 2019 21:51:32 +0800 Subject: [PATCH 06/13] put shutdown filter into default filter --- common/constant/default.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/constant/default.go b/common/constant/default.go index cb6d68af05..cb66f5f0ab 100644 --- a/common/constant/default.go +++ b/common/constant/default.go @@ -46,8 +46,8 @@ const ( const ( DEFAULT_KEY = "default" PREFIX_DEFAULT_KEY = "default." - DEFAULT_SERVICE_FILTERS = "echo,token,accesslog,tps,execute" - DEFAULT_REFERENCE_FILTERS = "" + DEFAULT_SERVICE_FILTERS = "echo,token,accesslog,tps,execute,pshutdown" + DEFAULT_REFERENCE_FILTERS = "cshutdown" GENERIC_REFERENCE_FILTERS = "generic" GENERIC = "$invoke" ECHO = "$echo" From 2eca33837cdec41d08aec260675b916c4bfd8746 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 14 Nov 2019 22:03:29 +0800 Subject: [PATCH 07/13] Add UT for shutdown_config --- common/extension/graceful_shutdown.go | 2 - config/graceful_shutdown.go | 11 ++--- config/graceful_shutdown_config_test.go | 48 ++++++++++++++++++++++ config/graceful_shutdown_test.go | 54 +++++++++++++++++++++---- 4 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 config/graceful_shutdown_config_test.go diff --git a/common/extension/graceful_shutdown.go b/common/extension/graceful_shutdown.go index ff34f5073e..02fedc6838 100644 --- a/common/extension/graceful_shutdown.go +++ b/common/extension/graceful_shutdown.go @@ -22,8 +22,6 @@ import ( ) var ( - // SystemShutdownCallbackNames = []string{"registry"} - // systemShutdownCallbacks = make(map[string]func()) customShutdownCallbacks = list.New() ) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index 8cf4083eb5..7e072d628d 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -116,7 +116,7 @@ func BeforeShutdown() { // If this application is not the consumer, it will do nothing destroyConsumerProtocols() - logger.Infof("Execute the custom callbacks.") + logger.Infof("Graceful shutdown --- Execute the custom callbacks.") customCallbacks := extension.GetAllCustomShutdownCallbacks() for callback := customCallbacks.Front(); callback != nil; callback = callback.Next() { callback.Value.(func())() @@ -152,12 +152,12 @@ func destroyProviderProtocols() { return } - consumerProtocol := make(map[string]interface{}, 0) + consumerProtocol := make(map[interface{}]interface{}, 0) if consumerConfig != nil && consumerConfig.ProtocolConf != nil { - consumerProtocol = consumerConfig.ProtocolConf.(map[string]interface{}) + consumerProtocol = consumerConfig.ProtocolConf.(map[interface{}]interface{}) } - protocols := providerConfig.ProtocolConf.(map[string]interface{}) + protocols := providerConfig.ProtocolConf.(map[interface{}]interface{}) for name, _ := range protocols { _, found := consumerProtocol[name] @@ -165,7 +165,7 @@ func destroyProviderProtocols() { if found { continue } - extension.GetProtocol(name).Destroy() + extension.GetProtocol(name.(string)).Destroy() } } @@ -202,6 +202,7 @@ func waitForSendingRequests() { // ignore this step return } + waitingProcessedTimeout(consumerConfig.ShutdownConfig) } func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) { diff --git a/config/graceful_shutdown_config_test.go b/config/graceful_shutdown_config_test.go new file mode 100644 index 0000000000..6e84f58692 --- /dev/null +++ b/config/graceful_shutdown_config_test.go @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "testing" + "time" +) +import ( + "github.com/stretchr/testify/assert" +) + +func TestShutdownConfig_GetTimeout(t *testing.T) { + config := ShutdownConfig{} + assert.False(t, config.RejectRequest) + assert.False(t, config.RequestsFinished) + + config = ShutdownConfig{ + Timeout: "12x", + StepTimeout: "34a", + } + + assert.Equal(t, 60*time.Second, config.GetTimeout()) + assert.Equal(t, 10*time.Second, config.GetStepTimeout()) + + config = ShutdownConfig{ + Timeout: "34", + StepTimeout: "79", + } + + assert.Equal(t, 34*time.Millisecond, config.GetTimeout()) + assert.Equal(t, 79*time.Millisecond, config.GetStepTimeout()) +} diff --git a/config/graceful_shutdown_test.go b/config/graceful_shutdown_test.go index 5958562089..981de6b608 100644 --- a/config/graceful_shutdown_test.go +++ b/config/graceful_shutdown_test.go @@ -19,12 +19,16 @@ package config import ( "testing" - +) +import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/protocol" ) +func TestGracefulShutdownInit(t *testing.T) { + GracefulShutdownInit() +} func TestBeforeShutdown(t *testing.T) { extension.SetProtocol("registry", func() protocol.Protocol { @@ -34,23 +38,57 @@ func TestBeforeShutdown(t *testing.T) { return &mockRegistryProtocol{} }) + extension.SetProtocol("mock", func() protocol.Protocol { + return &mockRegistryProtocol{} + }) + + + protocolConfigs := make(map[interface{}]interface{}) protocolConfigs[constant.DUBBO] = "aaa" + + // without configuration + BeforeShutdown() + + consumerConfig = &ConsumerConfig{ + ProtocolConf: protocolConfigs, + ShutdownConfig: &ShutdownConfig{ + Timeout: "1", + StepTimeout: "1000", + }, + } + + providerProtocols := make(map[interface{}]interface{}) + providerProtocols[constant.DUBBO] = "aaa" + + providerProtocols["mock"] = "aaa" + providerConfig = &ProviderConfig{ ShutdownConfig: &ShutdownConfig{ - Timeout: "1", - AcceptNewRequestsTimeout: "1", - WaitingProcessRequestsTimeout: "1", + Timeout: "1", + StepTimeout: "1000", + }, + ProtocolConf: providerProtocols, + } + // test destroy protocol + BeforeShutdown() + + providerConfig = &ProviderConfig{ + ShutdownConfig: &ShutdownConfig{ + Timeout: "1", + StepTimeout: "-1", }, ProtocolConf: protocolConfigs, } + consumerConfig = &ConsumerConfig{ ProtocolConf: protocolConfigs, ShutdownConfig: &ShutdownConfig{ - Timeout: "1", - AcceptNewRequestsTimeout: "1", - WaitingProcessRequestsTimeout: "1", + Timeout: "1", + StepTimeout: "-1", }, } + + // test ignore steps BeforeShutdown() -} \ No newline at end of file +} From 5289c063bbcd8024cac9bec4b439ba359d7b83dc Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 14 Nov 2019 22:55:58 +0800 Subject: [PATCH 08/13] Add UT --- config/graceful_shutdown.go | 4 +- config/graceful_shutdown_config.go | 4 +- ...nFilter.go => graceful_shutdown_filter.go} | 8 +- filter/impl/graceful_shutdown_filter_test.go | 74 +++++++++++++++++++ protocol/rpc_status.go | 14 ---- 5 files changed, 84 insertions(+), 20 deletions(-) rename filter/impl/{GracefulShutdownFilter.go => graceful_shutdown_filter.go} (93%) create mode 100644 filter/impl/graceful_shutdown_filter_test.go diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index 7e072d628d..eeb559b785 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -21,10 +21,11 @@ import ( "os" "os/signal" "runtime/debug" - "sync" "syscall" "time" +) +import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" @@ -45,7 +46,6 @@ import ( * So this signal will be ignored. * */ -var gracefulShutdownOnce = sync.Once{} func GracefulShutdownInit() { diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go index c5c16add05..c16123624f 100644 --- a/config/graceful_shutdown_config.go +++ b/config/graceful_shutdown_config.go @@ -20,10 +20,10 @@ package config import ( "strconv" "time" - +) +import ( "github.com/apache/dubbo-go/common/logger" ) - const ( defaultTimeout = 60 * time.Second defaultStepTimeout = 10 * time.Second diff --git a/filter/impl/GracefulShutdownFilter.go b/filter/impl/graceful_shutdown_filter.go similarity index 93% rename from filter/impl/GracefulShutdownFilter.go rename to filter/impl/graceful_shutdown_filter.go index 7337b20d0d..285fe11da8 100644 --- a/filter/impl/GracefulShutdownFilter.go +++ b/filter/impl/graceful_shutdown_filter.go @@ -19,7 +19,9 @@ package impl import ( "sync/atomic" +) +import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" "github.com/apache/dubbo-go/common/logger" @@ -34,7 +36,9 @@ func init() { activeCount: 0, shutdownConfig: config.GetConsumerConfig().ShutdownConfig, } - var providerFilter = &gracefulShutdownFilter{activeCount: 0} + var providerFilter = &gracefulShutdownFilter{activeCount: 0, + shutdownConfig: config.GetProviderConfig().ShutdownConfig, + } extension.SetFilter(constant.CONSUMER_SHUTDOWN_FILTER, func() filter.Filter { return consumerFiler @@ -62,7 +66,7 @@ func (gf *gracefulShutdownFilter) Invoke(invoker protocol.Invoker, invocation pr func (gf *gracefulShutdownFilter) OnResponse(result protocol.Result, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { atomic.AddInt32(&gf.activeCount, -1) // although this isn't thread safe, it won't be a problem if the gf.rejectNewRequest() is true. - if gf.activeCount <= 0 { + if gf.shutdownConfig != nil && gf.activeCount <= 0 { gf.shutdownConfig.RequestsFinished = true } return result diff --git a/filter/impl/graceful_shutdown_filter_test.go b/filter/impl/graceful_shutdown_filter_test.go new file mode 100644 index 0000000000..61a301dc5f --- /dev/null +++ b/filter/impl/graceful_shutdown_filter_test.go @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package impl + +import ( + "net/url" + "testing" +) +import ( + "github.com/stretchr/testify/assert" +) +import ( + "github.com/apache/dubbo-go/common" + "github.com/apache/dubbo-go/common/constant" + "github.com/apache/dubbo-go/common/extension" + "github.com/apache/dubbo-go/config" + filterCommon "github.com/apache/dubbo-go/filter/common" + "github.com/apache/dubbo-go/filter/common/impl" + "github.com/apache/dubbo-go/protocol" + "github.com/apache/dubbo-go/protocol/invocation" +) + +func TestGenericFilter_Invoke(t *testing.T) { + invoc := invocation.NewRPCInvocation("GetUser", []interface{}{"OK"}, make(map[string]string, 0)) + + invokeUrl := common.NewURLWithOptions( + common.WithParams(url.Values{}), ) + + shutdownFilter := extension.GetFilter(constant.PROVIDER_SHUTDOWN_FILTER).(*gracefulShutdownFilter) + + providerConfig := config.GetProviderConfig() + + assert.False(t, shutdownFilter.rejectNewRequest()) + assert.Nil(t, providerConfig.ShutdownConfig) + + assert.Equal(t, extension.GetRejectedExecutionHandler(constant.DEFAULT_KEY), + shutdownFilter.getRejectHandler()) + + result := shutdownFilter.Invoke(protocol.NewBaseInvoker(*invokeUrl), invoc) + assert.NotNil(t, result) + assert.Nil(t, result.Error()) + + providerConfig.ShutdownConfig = &config.ShutdownConfig{ + RejectRequest: true, + RejectRequestHandler: "mock", + } + shutdownFilter.shutdownConfig = providerConfig.ShutdownConfig + + assert.True(t, shutdownFilter.rejectNewRequest()) + result = shutdownFilter.OnResponse(nil, protocol.NewBaseInvoker(*invokeUrl), invoc) + + rejectHandler := &impl.OnlyLogRejectedExecutionHandler{} + extension.SetRejectedExecutionHandler("mock", func() filterCommon.RejectedExecutionHandler { + return rejectHandler + }) + assert.True(t, providerConfig.ShutdownConfig.RequestsFinished) + assert.Equal(t, rejectHandler, shutdownFilter.getRejectHandler()) + +} diff --git a/protocol/rpc_status.go b/protocol/rpc_status.go index 4f7e63f9f0..3a8bfbc87f 100644 --- a/protocol/rpc_status.go +++ b/protocol/rpc_status.go @@ -73,17 +73,3 @@ func beginCount0(rpcStatus *RpcStatus) { func endCount0(rpcStatus *RpcStatus) { atomic.AddInt32(&rpcStatus.active, -1) } - -func GetTotalActive() int32 { - var result int32 = 0 - methodStatistics.Range(func(_, value interface{}) bool { - statics := value.(*sync.Map) - statics.Range(func(_, value interface{}) bool { - rpcStatus := value.(*RpcStatus) - result = result + rpcStatus.active - return true - }) - return true - }) - return result -} From a993531e75034edbcd9139c1554af0fe59000448 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 14 Nov 2019 23:32:01 +0800 Subject: [PATCH 09/13] Fix UT --- common/constant/key.go | 4 ++-- common/extension/graceful_shutdown.go | 4 ++-- config/consumer_config.go | 12 ++++++------ config/graceful_shutdown_config.go | 17 +++++++++-------- config/graceful_shutdown_config_test.go | 4 ++-- config/graceful_shutdown_test.go | 10 ++++------ config/provider_config.go | 12 ++++++------ filter/impl/graceful_shutdown_filter_test.go | 2 +- 8 files changed, 32 insertions(+), 33 deletions(-) diff --git a/common/constant/key.go b/common/constant/key.go index 2bfebf8f07..122b8e5056 100644 --- a/common/constant/key.go +++ b/common/constant/key.go @@ -71,8 +71,8 @@ const ( EXECUTE_LIMIT_KEY = "execute.limit" DEFAULT_EXECUTE_LIMIT = "-1" EXECUTE_REJECTED_EXECUTION_HANDLER_KEY = "execute.limit.rejected.handler" - PROVIDER_SHUTDOWN_FILTER = "pshutdown" - CONSUMER_SHUTDOWN_FILTER = "cshutdown" + PROVIDER_SHUTDOWN_FILTER = "pshutdown" + CONSUMER_SHUTDOWN_FILTER = "cshutdown" ) const ( diff --git a/common/extension/graceful_shutdown.go b/common/extension/graceful_shutdown.go index 02fedc6838..c8807fcc28 100644 --- a/common/extension/graceful_shutdown.go +++ b/common/extension/graceful_shutdown.go @@ -22,7 +22,7 @@ import ( ) var ( - customShutdownCallbacks = list.New() + customShutdownCallbacks = list.New() ) /** @@ -44,7 +44,7 @@ var ( * the benefit of that mechanism is low. * And it may introduce much complication for another users. */ -func AddCustomShutdownCallback(callback func()) { +func AddCustomShutdownCallback(callback func()) { customShutdownCallbacks.PushBack(callback) } diff --git a/config/consumer_config.go b/config/consumer_config.go index 54b87e22f4..72f60b5f77 100644 --- a/config/consumer_config.go +++ b/config/consumer_config.go @@ -52,12 +52,12 @@ type ConsumerConfig struct { ProxyFactory string `yaml:"proxy_factory" default:"default" json:"proxy_factory,omitempty" property:"proxy_factory"` Check *bool `yaml:"check" json:"check,omitempty" property:"check"` - Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` - Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` - References map[string]*ReferenceConfig `yaml:"references" json:"references,omitempty" property:"references"` - ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf"` - FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` - ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` + Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` + Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` + References map[string]*ReferenceConfig `yaml:"references" json:"references,omitempty" property:"references"` + ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf"` + FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` } func (c *ConsumerConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go index c16123624f..d7d21c1499 100644 --- a/config/graceful_shutdown_config.go +++ b/config/graceful_shutdown_config.go @@ -18,16 +18,17 @@ package config import ( - "strconv" "time" ) import ( "github.com/apache/dubbo-go/common/logger" ) + const ( - defaultTimeout = 60 * time.Second + defaultTimeout = 60 * time.Second defaultStepTimeout = 10 * time.Second ) + type ShutdownConfig struct { /* * Total timeout. Even though we don't release all resources, @@ -35,7 +36,7 @@ type ShutdownConfig struct { * default value is 60 * 1000 ms = 1 minutes * In general, it should be bigger than 3 * StepTimeout. */ - Timeout string `yaml:"timeout" json:"timeout,omitempty" property:"timeout"` + Timeout string `default:"60s" yaml:"timeout" json:"timeout,omitempty" property:"timeout"` /* * the timeout on each step. You should evaluate the response time of request * and the time that client noticed that server shutdown. @@ -43,7 +44,7 @@ type ShutdownConfig struct { * and the 99.9% requests will return response in 2s, so the StepTimeout will be bigger than(10+2) * 1000ms, * maybe (10 + 2*3) * 1000ms is a good choice. */ - StepTimeout string `yaml:"step_timeout" json:"step.timeout,omitempty" property:"step.timeout"` + StepTimeout string `default:"10s" yaml:"step_timeout" json:"step.timeout,omitempty" property:"step.timeout"` // when we try to shutdown the application, we will reject the new requests. In most cases, you don't need to configure this. RejectRequestHandler string `yaml:"reject_handler" json:"reject_handler,omitempty" property:"reject_handler"` // true -> new request will be rejected. @@ -55,21 +56,21 @@ type ShutdownConfig struct { } func (config *ShutdownConfig) GetTimeout() time.Duration { - result, err := strconv.ParseInt(config.Timeout, 0, 0) + result, err := time.ParseDuration(config.Timeout) if err != nil { logger.Errorf("The Timeout configuration is invalid: %s, and we will use the default value: %s", config.Timeout, defaultTimeout.String(), err) return defaultTimeout } - return time.Millisecond * time.Duration(result) + return result } func (config *ShutdownConfig) GetStepTimeout() time.Duration { - result, err := strconv.ParseInt(config.StepTimeout, 0, 0) + result, err := time.ParseDuration(config.StepTimeout) if err != nil { logger.Errorf("The StepTimeout configuration is invalid: %s, and we will use the default value: %s", config.Timeout, defaultStepTimeout.String(), err) return defaultStepTimeout } - return time.Millisecond * time.Duration(result) + return result } diff --git a/config/graceful_shutdown_config_test.go b/config/graceful_shutdown_config_test.go index 6e84f58692..4b6645db03 100644 --- a/config/graceful_shutdown_config_test.go +++ b/config/graceful_shutdown_config_test.go @@ -39,8 +39,8 @@ func TestShutdownConfig_GetTimeout(t *testing.T) { assert.Equal(t, 10*time.Second, config.GetStepTimeout()) config = ShutdownConfig{ - Timeout: "34", - StepTimeout: "79", + Timeout: "34ms", + StepTimeout: "79ms", } assert.Equal(t, 34*time.Millisecond, config.GetTimeout()) diff --git a/config/graceful_shutdown_test.go b/config/graceful_shutdown_test.go index 981de6b608..ca01f68142 100644 --- a/config/graceful_shutdown_test.go +++ b/config/graceful_shutdown_test.go @@ -42,8 +42,6 @@ func TestBeforeShutdown(t *testing.T) { return &mockRegistryProtocol{} }) - - protocolConfigs := make(map[interface{}]interface{}) protocolConfigs[constant.DUBBO] = "aaa" @@ -54,7 +52,7 @@ func TestBeforeShutdown(t *testing.T) { ProtocolConf: protocolConfigs, ShutdownConfig: &ShutdownConfig{ Timeout: "1", - StepTimeout: "1000", + StepTimeout: "1s", }, } @@ -66,7 +64,7 @@ func TestBeforeShutdown(t *testing.T) { providerConfig = &ProviderConfig{ ShutdownConfig: &ShutdownConfig{ Timeout: "1", - StepTimeout: "1000", + StepTimeout: "1s", }, ProtocolConf: providerProtocols, } @@ -76,7 +74,7 @@ func TestBeforeShutdown(t *testing.T) { providerConfig = &ProviderConfig{ ShutdownConfig: &ShutdownConfig{ Timeout: "1", - StepTimeout: "-1", + StepTimeout: "-1s", }, ProtocolConf: protocolConfigs, } @@ -85,7 +83,7 @@ func TestBeforeShutdown(t *testing.T) { ProtocolConf: protocolConfigs, ShutdownConfig: &ShutdownConfig{ Timeout: "1", - StepTimeout: "-1", + StepTimeout: "-1s", }, } diff --git a/config/provider_config.go b/config/provider_config.go index d1de691be7..0fed44c81b 100644 --- a/config/provider_config.go +++ b/config/provider_config.go @@ -44,12 +44,12 @@ type ProviderConfig struct { ApplicationConfig *ApplicationConfig `yaml:"application" json:"application,omitempty" property:"application"` Registry *RegistryConfig `yaml:"registry" json:"registry,omitempty" property:"registry"` - Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` - Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` - Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` - ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` - FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` - ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` + Registries map[string]*RegistryConfig `yaml:"registries" json:"registries,omitempty" property:"registries"` + Services map[string]*ServiceConfig `yaml:"services" json:"services,omitempty" property:"services"` + Protocols map[string]*ProtocolConfig `yaml:"protocols" json:"protocols,omitempty" property:"protocols"` + ProtocolConf interface{} `yaml:"protocol_conf" json:"protocol_conf,omitempty" property:"protocol_conf" ` + FilterConf interface{} `yaml:"filter_conf" json:"filter_conf,omitempty" property:"filter_conf" ` + ShutdownConfig *ShutdownConfig `yaml:"shutdown_conf" json:"shutdown_conf,omitempty" property:"shutdown_conf" ` } func (c *ProviderConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/filter/impl/graceful_shutdown_filter_test.go b/filter/impl/graceful_shutdown_filter_test.go index 61a301dc5f..f541219b5a 100644 --- a/filter/impl/graceful_shutdown_filter_test.go +++ b/filter/impl/graceful_shutdown_filter_test.go @@ -39,7 +39,7 @@ func TestGenericFilter_Invoke(t *testing.T) { invoc := invocation.NewRPCInvocation("GetUser", []interface{}{"OK"}, make(map[string]string, 0)) invokeUrl := common.NewURLWithOptions( - common.WithParams(url.Values{}), ) + common.WithParams(url.Values{})) shutdownFilter := extension.GetFilter(constant.PROVIDER_SHUTDOWN_FILTER).(*gracefulShutdownFilter) From af9741ad5d197ee1e1273399e448d94355e67c6f Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Fri, 15 Nov 2019 00:02:48 +0800 Subject: [PATCH 10/13] Finish Test --- common/constant/key.go | 1 + config/base_config_test.go | 8 ++++++ config/graceful_shutdown.go | 41 +++++++++++++++--------------- config/graceful_shutdown_config.go | 5 ++++ registry/zookeeper/listener.go | 8 +++++- registry/zookeeper/registry.go | 11 +++----- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/common/constant/key.go b/common/constant/key.go index 122b8e5056..a275c6135f 100644 --- a/common/constant/key.go +++ b/common/constant/key.go @@ -114,6 +114,7 @@ const ( ProtocolConfigPrefix = "dubbo.protocols." ProviderConfigPrefix = "dubbo.provider." ConsumerConfigPrefix = "dubbo.consumer." + ShutdownConfigPrefix = "dubbo.shutdown." ) const ( diff --git a/config/base_config_test.go b/config/base_config_test.go index 6dc3749e55..3684437d36 100644 --- a/config/base_config_test.go +++ b/config/base_config_test.go @@ -39,6 +39,7 @@ func Test_refresh(t *testing.T) { mockMap["dubbo.com.MockService.MockService.GetUser.retries"] = "10" mockMap["dubbo.consumer.check"] = "false" mockMap["dubbo.application.name"] = "dubbo" + mockMap["dubbo.shutdown.timeout"] = "12s" config.GetEnvInstance().UpdateExternalConfigMap(mockMap) @@ -113,6 +114,13 @@ func Test_refresh(t *testing.T) { }, }, }, + ShutdownConfig: &ShutdownConfig{ + Timeout: "12s", + StepTimeout: "2s", + RejectRequestHandler: "mock", + RejectRequest: false, + RequestsFinished: false, + }, } c.SetFatherConfig(father) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index eeb559b785..bbc7e64a7a 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -79,24 +79,6 @@ func GracefulShutdownInit() { }() } -func totalTimeout() time.Duration { - var providerShutdown time.Duration = 0 - if providerConfig != nil && providerConfig.ShutdownConfig != nil { - providerShutdown = providerConfig.ShutdownConfig.GetTimeout() - } - - var consumerShutdown time.Duration = 0 - if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { - consumerShutdown = consumerConfig.ShutdownConfig.GetTimeout() - } - - var timeout = providerShutdown - if consumerShutdown > providerShutdown { - timeout = consumerShutdown - } - return timeout -} - func BeforeShutdown() { destroyAllRegistries() @@ -104,6 +86,7 @@ func BeforeShutdown() { // The value of configuration depends on how long the clients will get notification. waitAndAcceptNewRequests() + time.Sleep(1 * time.Minute) // reject the new request, but keeping waiting for accepting requests waitForReceivingRequests() @@ -116,7 +99,7 @@ func BeforeShutdown() { // If this application is not the consumer, it will do nothing destroyConsumerProtocols() - logger.Infof("Graceful shutdown --- Execute the custom callbacks.") + logger.Info("Graceful shutdown --- Execute the custom callbacks.") customCallbacks := extension.GetAllCustomShutdownCallbacks() for callback := customCallbacks.Front(); callback != nil; callback = callback.Next() { callback.Value.(func())() @@ -124,7 +107,7 @@ func BeforeShutdown() { } func destroyAllRegistries() { - logger.Infof("Graceful shutdown --- Destroy all registries. ") + logger.Info("Graceful shutdown --- Destroy all registries. ") registryProtocol := extension.GetProtocol(constant.REGISTRY_KEY) registryProtocol.Destroy() } @@ -216,3 +199,21 @@ func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) { time.Sleep(10 * time.Millisecond) } } + +func totalTimeout() time.Duration { + var providerShutdown time.Duration = 0 + if providerConfig != nil && providerConfig.ShutdownConfig != nil { + providerShutdown = providerConfig.ShutdownConfig.GetTimeout() + } + + var consumerShutdown time.Duration = 0 + if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { + consumerShutdown = consumerConfig.ShutdownConfig.GetTimeout() + } + + var timeout = providerShutdown + if consumerShutdown > providerShutdown { + timeout = consumerShutdown + } + return timeout +} diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go index d7d21c1499..9077752ce2 100644 --- a/config/graceful_shutdown_config.go +++ b/config/graceful_shutdown_config.go @@ -21,6 +21,7 @@ import ( "time" ) import ( + "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/logger" ) @@ -55,6 +56,10 @@ type ShutdownConfig struct { RequestsFinished bool } +func (config *ShutdownConfig) Prefix() string { + return constant.ShutdownConfigPrefix +} + func (config *ShutdownConfig) GetTimeout() time.Duration { result, err := time.ParseDuration(config.Timeout) if err != nil { diff --git a/registry/zookeeper/listener.go b/registry/zookeeper/listener.go index 67479c4436..5a4cc2c66e 100644 --- a/registry/zookeeper/listener.go +++ b/registry/zookeeper/listener.go @@ -109,7 +109,13 @@ func (l *RegistryConfigurationListener) Next() (*registry.ServiceEvent, error) { } } func (l *RegistryConfigurationListener) Close() { - l.registry.wg.Done() + if l.registry.IsAvailable() { + /** + * if the registry is not available, it means that the registry has been destroy + * so we don't need to call Done(), or it will cause the negative count panic for registry.wg + */ + l.registry.wg.Done() + } } func (l *RegistryConfigurationListener) valid() bool { diff --git a/registry/zookeeper/registry.go b/registry/zookeeper/registry.go index 682b9fe0cf..29ae51d44f 100644 --- a/registry/zookeeper/registry.go +++ b/registry/zookeeper/registry.go @@ -173,14 +173,9 @@ func (r *zkRegistry) GetUrl() common.URL { } func (r *zkRegistry) Destroy() { - /** - * Don't r.listener.Close() - * here we don't close the listener because - * the listener will be close in Subscribe(). - * We can not close it here. If we do that, - * a negative count error of r.wg will occur because - * we close the listener twice. - */ + if r.configListener != nil { + r.configListener.Close() + } close(r.done) r.wg.Wait() r.closeRegisters() From 3422685482757e74391efbf549ae12d8d09c7917 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Fri, 15 Nov 2019 18:11:36 +0800 Subject: [PATCH 11/13] Add Configuration example --- config/graceful_shutdown.go | 5 +++-- config/testdata/consumer_config.yml | 4 ++++ config/testdata/consumer_config_with_configcenter.yml | 4 ++++ config/testdata/consumer_config_withoutProtocol.yml | 4 ++++ config/testdata/provider_config.yml | 4 ++++ config/testdata/provider_config_withoutProtocol.yml | 4 ++++ 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index bbc7e64a7a..dc715d5ed2 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -42,6 +42,7 @@ import ( * * So the signals SIGKILL, SIGSTOP, SIGHUP, SIGINT, SIGTERM, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT, SIGSYS * should be processed. + * syscall.SIGEMT cannot be found in CI * It's seems that the Unix/Linux does not have the signal SIGSTKFLT. https://github.com/golang/go/issues/33381 * So this signal will be ignored. * @@ -53,7 +54,7 @@ func GracefulShutdownInit() { signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGKILL, syscall.SIGSTOP, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, - syscall.SIGABRT, syscall.SIGEMT, syscall.SIGSYS, + syscall.SIGABRT, syscall.SIGSYS, ) go func() { @@ -66,7 +67,7 @@ func GracefulShutdownInit() { switch sig { // those signals' original behavior is exit with dump ths stack, so we try to keep the behavior case syscall.SIGQUIT, syscall.SIGILL, syscall.SIGTRAP, - syscall.SIGABRT, syscall.SIGEMT, syscall.SIGSYS: + syscall.SIGABRT, syscall.SIGSYS: debug.WriteHeapDump(os.Stdout.Fd()) default: time.AfterFunc(totalTimeout(), func() { diff --git a/config/testdata/consumer_config.yml b/config/testdata/consumer_config.yml index 9fd50bb4d3..f44ea449fd 100644 --- a/config/testdata/consumer_config.yml +++ b/config/testdata/consumer_config.yml @@ -49,6 +49,10 @@ references: "soa.com.ikurento.user.UserProvider" "forks": 5 +shutdown_conf: + timeout: 60s + step_timeout: 10s + protocol_conf: dubbo: reconnect_interval: 0 diff --git a/config/testdata/consumer_config_with_configcenter.yml b/config/testdata/consumer_config_with_configcenter.yml index 0550cc8974..ebe56fa93f 100644 --- a/config/testdata/consumer_config_with_configcenter.yml +++ b/config/testdata/consumer_config_with_configcenter.yml @@ -17,6 +17,10 @@ references: - name: "GetUser" retries: "3" +shutdown_conf: + timeout: 60s + step_timeout: 10s + protocol_conf: dubbo: reconnect_interval: 0 diff --git a/config/testdata/consumer_config_withoutProtocol.yml b/config/testdata/consumer_config_withoutProtocol.yml index 5e57c7ddf6..32bad8b91d 100644 --- a/config/testdata/consumer_config_withoutProtocol.yml +++ b/config/testdata/consumer_config_withoutProtocol.yml @@ -48,6 +48,10 @@ references: "soa.com.ikurento.user.UserProvider" "forks": 5 +shutdown_conf: + timeout: 60s + step_timeout: 10s + protocol_conf: dubbo: reconnect_interval: 0 diff --git a/config/testdata/provider_config.yml b/config/testdata/provider_config.yml index 080feb7dcd..7c46f9101a 100644 --- a/config/testdata/provider_config.yml +++ b/config/testdata/provider_config.yml @@ -71,6 +71,10 @@ protocols: # ip: "127.0.0.1" # port: 20001 +shutdown_conf: + timeout: 60s + step_timeout: 10s + protocol_conf: dubbo: session_number: 700 diff --git a/config/testdata/provider_config_withoutProtocol.yml b/config/testdata/provider_config_withoutProtocol.yml index 2f65868d49..532d3005aa 100644 --- a/config/testdata/provider_config_withoutProtocol.yml +++ b/config/testdata/provider_config_withoutProtocol.yml @@ -51,6 +51,10 @@ protocols: # ip: "127.0.0.1" # port: 20001 +shutdown_conf: + timeout: 60s + step_timeout: 10s + protocol_conf: dubbo: session_number: 700 From a17a95aee0b88e053e3a1a07be5067b9b0c3967b Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Sun, 17 Nov 2019 20:43:45 +0800 Subject: [PATCH 12/13] Fix review comments --- config/graceful_shutdown.go | 60 ++++++++++++-------- config/graceful_shutdown_config.go | 7 ++- config/graceful_shutdown_config_test.go | 1 + config/graceful_shutdown_test.go | 5 +- filter/impl/graceful_shutdown_filter.go | 3 +- filter/impl/graceful_shutdown_filter_test.go | 2 + 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index dc715d5ed2..d12c8dfb1f 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -25,6 +25,10 @@ import ( "time" ) +import ( + "github.com/dubbogo/gost/container/gxset" +) + import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" @@ -60,7 +64,7 @@ func GracefulShutdownInit() { go func() { select { case sig := <-signals: - logger.Infof("get signal %s, application will shutdown.", sig.String()) + logger.Infof("get signal %s, application will shutdown.", sig) // gracefulShutdownOnce.Do(func() { BeforeShutdown() @@ -87,18 +91,20 @@ func BeforeShutdown() { // The value of configuration depends on how long the clients will get notification. waitAndAcceptNewRequests() - time.Sleep(1 * time.Minute) // reject the new request, but keeping waiting for accepting requests waitForReceivingRequests() + // we fetch the protocols from Consumer.References. Consumer.ProtocolConfig doesn't contains all protocol, like jsonrpc + consumerProtocols := getConsumerProtocols() + // If this application is not the provider, it will do nothing - destroyProviderProtocols() + destroyProviderProtocols(consumerProtocols) // reject sending the new request, and waiting for response of sending requests waitForSendingRequests() // If this application is not the consumer, it will do nothing - destroyConsumerProtocols() + destroyConsumerProtocols(consumerProtocols) logger.Info("Graceful shutdown --- Execute the custom callbacks.") customCallbacks := extension.GetAllCustomShutdownCallbacks() @@ -113,13 +119,9 @@ func destroyAllRegistries() { registryProtocol.Destroy() } -func destroyConsumerProtocols() { +func destroyConsumerProtocols(consumerProtocols *gxset.HashSet) { logger.Info("Graceful shutdown --- Destroy consumer's protocols. ") - if consumerConfig == nil || consumerConfig.ProtocolConf == nil { - return - } - protocols := consumerConfig.ProtocolConf.(map[interface{}]interface{}) - for name, _ := range protocols { + for name := range consumerProtocols.Items { extension.GetProtocol(name.(string)).Destroy() } } @@ -128,7 +130,7 @@ func destroyConsumerProtocols() { * destroy the provider's protocol. * if the protocol is consumer's protocol too, we will keep it. */ -func destroyProviderProtocols() { +func destroyProviderProtocols(consumerProtocols *gxset.HashSet) { logger.Info("Graceful shutdown --- Destroy provider's protocols. ") @@ -136,17 +138,11 @@ func destroyProviderProtocols() { return } - consumerProtocol := make(map[interface{}]interface{}, 0) - if consumerConfig != nil && consumerConfig.ProtocolConf != nil { - consumerProtocol = consumerConfig.ProtocolConf.(map[interface{}]interface{}) - } - protocols := providerConfig.ProtocolConf.(map[interface{}]interface{}) - for name, _ := range protocols { - _, found := consumerProtocol[name] + for name := range protocols { - // the protocol is the consumer's protocol, we can not destroy it. - if found { + // the protocol is the consumer's protocol too, we can not destroy it. + if consumerProtocols.Contains(name) { continue } extension.GetProtocol(name.(string)).Destroy() @@ -194,20 +190,21 @@ func waitingProcessedTimeout(shutdownConfig *ShutdownConfig) { if timeout <= 0 { return } - start := time.Now().UnixNano() - for time.Now().UnixNano()-start < timeout.Nanoseconds() && !shutdownConfig.RequestsFinished { + start := time.Now() + + for time.Now().After(start.Add(timeout)) && !shutdownConfig.RequestsFinished { // sleep 10 ms and then we check it again time.Sleep(10 * time.Millisecond) } } func totalTimeout() time.Duration { - var providerShutdown time.Duration = 0 + var providerShutdown time.Duration if providerConfig != nil && providerConfig.ShutdownConfig != nil { providerShutdown = providerConfig.ShutdownConfig.GetTimeout() } - var consumerShutdown time.Duration = 0 + var consumerShutdown time.Duration if consumerConfig != nil && consumerConfig.ShutdownConfig != nil { consumerShutdown = consumerConfig.ShutdownConfig.GetTimeout() } @@ -218,3 +215,18 @@ func totalTimeout() time.Duration { } return timeout } + +/* + * we can not get the protocols from consumerConfig because some protocol don't have configuration, like jsonrpc. + */ +func getConsumerProtocols() *gxset.HashSet { + result := gxset.NewSet() + if consumerConfig == nil || consumerConfig.References == nil { + return result + } + + for _, reference := range consumerConfig.References { + result.Add(reference.Protocol) + } + return result +} diff --git a/config/graceful_shutdown_config.go b/config/graceful_shutdown_config.go index 9077752ce2..df55728565 100644 --- a/config/graceful_shutdown_config.go +++ b/config/graceful_shutdown_config.go @@ -20,6 +20,7 @@ package config import ( "time" ) + import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/logger" @@ -63,7 +64,7 @@ func (config *ShutdownConfig) Prefix() string { func (config *ShutdownConfig) GetTimeout() time.Duration { result, err := time.ParseDuration(config.Timeout) if err != nil { - logger.Errorf("The Timeout configuration is invalid: %s, and we will use the default value: %s", + logger.Errorf("The Timeout configuration is invalid: %s, and we will use the default value: %s, err: %v", config.Timeout, defaultTimeout.String(), err) return defaultTimeout } @@ -73,8 +74,8 @@ func (config *ShutdownConfig) GetTimeout() time.Duration { func (config *ShutdownConfig) GetStepTimeout() time.Duration { result, err := time.ParseDuration(config.StepTimeout) if err != nil { - logger.Errorf("The StepTimeout configuration is invalid: %s, and we will use the default value: %s", - config.Timeout, defaultStepTimeout.String(), err) + logger.Errorf("The StepTimeout configuration is invalid: %s, and we will use the default value: %s, err: %v", + config.StepTimeout, defaultStepTimeout.String(), err) return defaultStepTimeout } return result diff --git a/config/graceful_shutdown_config_test.go b/config/graceful_shutdown_config_test.go index 4b6645db03..583ed70b83 100644 --- a/config/graceful_shutdown_config_test.go +++ b/config/graceful_shutdown_config_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" ) + import ( "github.com/stretchr/testify/assert" ) diff --git a/config/graceful_shutdown_test.go b/config/graceful_shutdown_test.go index ca01f68142..0c4960152d 100644 --- a/config/graceful_shutdown_test.go +++ b/config/graceful_shutdown_test.go @@ -20,6 +20,7 @@ package config import ( "testing" ) + import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/extension" @@ -42,7 +43,7 @@ func TestBeforeShutdown(t *testing.T) { return &mockRegistryProtocol{} }) - protocolConfigs := make(map[interface{}]interface{}) + protocolConfigs := make(map[interface{}]interface{}, 16) protocolConfigs[constant.DUBBO] = "aaa" // without configuration @@ -56,7 +57,7 @@ func TestBeforeShutdown(t *testing.T) { }, } - providerProtocols := make(map[interface{}]interface{}) + providerProtocols := make(map[interface{}]interface{}, 16) providerProtocols[constant.DUBBO] = "aaa" providerProtocols["mock"] = "aaa" diff --git a/filter/impl/graceful_shutdown_filter.go b/filter/impl/graceful_shutdown_filter.go index 285fe11da8..b912ea88e4 100644 --- a/filter/impl/graceful_shutdown_filter.go +++ b/filter/impl/graceful_shutdown_filter.go @@ -33,10 +33,9 @@ import ( func init() { var consumerFiler = &gracefulShutdownFilter{ - activeCount: 0, shutdownConfig: config.GetConsumerConfig().ShutdownConfig, } - var providerFilter = &gracefulShutdownFilter{activeCount: 0, + var providerFilter = &gracefulShutdownFilter{ shutdownConfig: config.GetProviderConfig().ShutdownConfig, } diff --git a/filter/impl/graceful_shutdown_filter_test.go b/filter/impl/graceful_shutdown_filter_test.go index f541219b5a..21da167ea0 100644 --- a/filter/impl/graceful_shutdown_filter_test.go +++ b/filter/impl/graceful_shutdown_filter_test.go @@ -21,9 +21,11 @@ import ( "net/url" "testing" ) + import ( "github.com/stretchr/testify/assert" ) + import ( "github.com/apache/dubbo-go/common" "github.com/apache/dubbo-go/common/constant" From d7b5f50999ef7f47bd7200c377202eff2956f981 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Fri, 22 Nov 2019 10:02:46 +0800 Subject: [PATCH 13/13] fix review comment --- config/graceful_shutdown.go | 9 ++++----- config/graceful_shutdown_test.go | 28 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/config/graceful_shutdown.go b/config/graceful_shutdown.go index d12c8dfb1f..0b39d11150 100644 --- a/config/graceful_shutdown.go +++ b/config/graceful_shutdown.go @@ -134,18 +134,17 @@ func destroyProviderProtocols(consumerProtocols *gxset.HashSet) { logger.Info("Graceful shutdown --- Destroy provider's protocols. ") - if providerConfig == nil || providerConfig.ProtocolConf == nil { + if providerConfig == nil || providerConfig.Protocols == nil { return } - protocols := providerConfig.ProtocolConf.(map[interface{}]interface{}) - for name := range protocols { + for _, protocol := range providerConfig.Protocols { // the protocol is the consumer's protocol too, we can not destroy it. - if consumerProtocols.Contains(name) { + if consumerProtocols.Contains(protocol.Name) { continue } - extension.GetProtocol(name.(string)).Destroy() + extension.GetProtocol(protocol.Name).Destroy() } } diff --git a/config/graceful_shutdown_test.go b/config/graceful_shutdown_test.go index 0c4960152d..de203572c7 100644 --- a/config/graceful_shutdown_test.go +++ b/config/graceful_shutdown_test.go @@ -43,31 +43,37 @@ func TestBeforeShutdown(t *testing.T) { return &mockRegistryProtocol{} }) - protocolConfigs := make(map[interface{}]interface{}, 16) - protocolConfigs[constant.DUBBO] = "aaa" + // protocolConfigs := make(map[interface{}]interface{}, 16) + consumerReferences := map[string]*ReferenceConfig{} + consumerReferences[constant.DUBBO] = &ReferenceConfig{ + Protocol: constant.DUBBO, + } // without configuration BeforeShutdown() consumerConfig = &ConsumerConfig{ - ProtocolConf: protocolConfigs, + References: consumerReferences, ShutdownConfig: &ShutdownConfig{ Timeout: "1", StepTimeout: "1s", - }, - } + }} - providerProtocols := make(map[interface{}]interface{}, 16) - providerProtocols[constant.DUBBO] = "aaa" + providerProtocols := map[string]*ProtocolConfig{} + providerProtocols[constant.DUBBO] = &ProtocolConfig{ + Name: constant.DUBBO, + } - providerProtocols["mock"] = "aaa" + providerProtocols["mock"] = &ProtocolConfig{ + Name: "mock", + } providerConfig = &ProviderConfig{ ShutdownConfig: &ShutdownConfig{ Timeout: "1", StepTimeout: "1s", }, - ProtocolConf: providerProtocols, + Protocols: providerProtocols, } // test destroy protocol BeforeShutdown() @@ -77,11 +83,11 @@ func TestBeforeShutdown(t *testing.T) { Timeout: "1", StepTimeout: "-1s", }, - ProtocolConf: protocolConfigs, + Protocols: providerProtocols, } consumerConfig = &ConsumerConfig{ - ProtocolConf: protocolConfigs, + References: consumerReferences, ShutdownConfig: &ShutdownConfig{ Timeout: "1", StepTimeout: "-1s",