This is an automated email from the ASF dual-hosted git repository.

xuetaoli pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go-samples.git


The following commit(s) were added to refs/heads/main by this push:
     new 33141f47 Feat/graceful shutdown sample (#1058)
33141f47 is described below

commit 33141f47838207638917edd8cbbb0ec2bc9a0066
Author: Oxidaner <[email protected]>
AuthorDate: Sun Apr 5 22:03:28 2026 +0800

    Feat/graceful shutdown sample (#1058)
    
    * Add graceful shutdown integration sample
    
    * refactor: narrow graceful shutdown sample to triple
    
    * fix: remove local replace from graceful shutdown sample
    
    * style: align graceful shutdown imports
    
    * test: add graceful shutdown integration coverage
    
    * test: refine integration script for graceful shutdown
    
    * test: use short connections in graceful shutdown integration
    
    * update greet.pb.go and greet.triple.go
---
 README.md                               |   1 +
 README_CN.md                            |   1 +
 graceful_shutdown/README.md             | 195 +++++++++++++++++++++++++++++
 graceful_shutdown/README_CN.md          | 210 ++++++++++++++++++++++++++++++++
 graceful_shutdown/go-client/cmd/main.go | 158 ++++++++++++++++++++++++
 graceful_shutdown/go-server/cmd/main.go | 107 ++++++++++++++++
 graceful_shutdown/proto/greet.pb.go     | 190 +++++++++++++++++++++++++++++
 graceful_shutdown/proto/greet.proto     |  36 ++++++
 graceful_shutdown/proto/greet.triple.go | 122 +++++++++++++++++++
 integrate_test.sh                       | 167 ++++++++++++++++++++++++-
 start_integrate_test.sh                 |   3 +
 11 files changed, 1189 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 7dfc6b2d..c7313344 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ Please refer to [HOWTO.md](HOWTO.md) for detailed 
instructions on running the sa
 * `direct`: Triple point-to-point invocation sample without a registry, also 
includes Go–Java interoperability.
 * `game`: Game service example.
 * `generic`: Generic invocation examples supporting interoperability between 
Dubbo-Go and Dubbo Java services, suitable for scenarios without interface 
information.
+* `graceful_shutdown`: Triple graceful shutdown sample for verifying 
long-connection notice, in-flight request draining, and shutdown timing knobs.
 * `integrate_test`: Integration test cases for Dubbo-go samples.
 * `java_interop`: Demonstrates interoperability between Java and Go Dubbo 
implementations.
   * `non-protobuf-dubbo`: Java/Go interoperability with the classic Dubbo 
protocol and non-protobuf payloads (Hessian2 style).
diff --git a/README_CN.md b/README_CN.md
index fd74ad3d..943a0dcc 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -32,6 +32,7 @@
 * `direct`:不依赖注册中心的 Triple 点对点调用示例,并包含 Go 与 Java 的互操作示例。
 * `game`:游戏服务示例。
 * `generic`:泛化调用示例,支持 Dubbo-Go 与 Dubbo Java 服务互操作,适用于无接口信息场景。
+* `graceful_shutdown`:Triple 优雅停机示例,用于验证长连接通知、请求排空,以及相关停机时间参数。
 * `integrate_test`:Dubbo-go 示例的集成测试用例。
 * `java_interop`:展示 Java 与 Go Dubbo 实现之间的互操作能力。
   * `non-protobuf-dubbo`:基于经典 Dubbo 协议与非 Protobuf 负载(Hessian2 风格)的 Java/Go 
互操作示例。
diff --git a/graceful_shutdown/README.md b/graceful_shutdown/README.md
new file mode 100644
index 00000000..c58026d6
--- /dev/null
+++ b/graceful_shutdown/README.md
@@ -0,0 +1,195 @@
+# Graceful Shutdown Example
+
+English | [中文](README_CN.md)
+
+This sample is intended for manual verification of the Triple graceful 
shutdown flow in `dubbo-go`.
+
+It is useful for verifying these behaviors:
+- active notice for long connections on Triple
+- passive closing behavior on the consumer side
+- waiting for in-flight provider requests during shutdown
+- the effect of `timeout`, `step-timeout`, `consumer-update-wait`, and 
`offline-window`
+
+This sample does **not** include a registry. That means you can test 
protocol-level active notice and request draining, but you cannot directly 
observe registry unregister propagation in this sample alone.
+
+## Prerequisites
+
+This sample uses the repository root `go.mod`.
+
+Run all commands from your local checkout of `dubbo-go-samples`:
+
+```bash
+cd /path/to/dubbo-go-samples
+```
+
+## Quick Start
+
+Start the server in one terminal:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=60s -step-timeout=5s 
-delay=2s
+```
+
+Start the client in another terminal:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=3 -interval=300ms -request-timeout=6s
+```
+
+Then press `Ctrl+C` in the server terminal.
+
+Expected result:
+- in-flight requests may still complete
+- new requests will begin to fail during shutdown
+- server logs will show the graceful shutdown phases in order
+
+## Important Address Format
+
+For direct client calls, always include the protocol prefix in `-addr`.
+
+Example:
+- Triple: `tri://127.0.0.1:20000`
+
+If you omit the protocol prefix and only pass `127.0.0.1:20000`, the direct 
reference may be parsed incorrectly in some scenarios.
+
+## Server Flags
+
+`graceful_shutdown/go-server/cmd/main.go` supports these test flags:
+- `-port=20000`
+- `-timeout=60s`
+- `-step-timeout=3s`
+- `-consumer-update-wait=3s`
+- `-offline-window=3s`
+- `-delay=0s`
+
+`-delay` adds artificial processing delay to every request so you can verify 
in-flight request draining.
+
+## Client Flags
+
+`graceful_shutdown/go-client/cmd/main.go` supports these test flags:
+- `-addr=tri://127.0.0.1:20000`
+- `-interval=200ms`
+- `-concurrency=1`
+- `-request-timeout=5s`
+- `-short=true|false`
+- `-name-prefix=hello`
+- `-max-requests=0`
+- `-min-successes=0`
+- `-min-failures=0`
+
+For long-connection testing, keep `-short=false`.
+
+`-max-requests`, `-min-successes`, and `-min-failures` are mainly for 
automated verification. The client panics if the configured minimum counts are 
not reached before exit.
+
+## Recommended Scenarios
+
+### 1. Triple active notice with long connection
+
+Terminal 1:
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=60s
+```
+
+Terminal 2:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=1 -interval=200ms
+```
+
+Then press `Ctrl+C` in the server terminal.
+
+What to observe:
+- server logs show the graceful shutdown phases
+- client starts failing shortly after shutdown begins
+- long connection is actively notified instead of only waiting for process exit
+
+### 2. In-flight request draining
+
+Terminal 1:
+```bash
+go run ./graceful_shutdown/go-server/cmd -delay=2s -step-timeout=5s
+```
+
+Terminal 2:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=3 -interval=300ms -request-timeout=6s
+```
+
+Then press `Ctrl+C` in the server terminal while requests are still running.
+
+What to observe:
+- already-running requests still have a chance to complete
+- new requests begin to fail during shutdown
+- the server exits after the in-flight wait budget is consumed or requests 
finish
+
+### 3. Observe active notice and request draining together
+
+Use a short consumer update wait so shutdown starts rejecting new work earlier 
while existing requests are still draining.
+
+Terminal 1:
+```bash
+go run ./graceful_shutdown/go-server/cmd -delay=2s -timeout=15s 
-step-timeout=2s -consumer-update-wait=0s
+```
+
+Terminal 2:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=2 -interval=200ms -request-timeout=4s
+```
+
+Then press `Ctrl+C` in the server terminal.
+
+What to observe:
+- server logs print the full graceful shutdown sequence
+- some in-flight requests still complete after shutdown starts
+- newer requests begin to fail earlier than in the default configuration
+- client logs include the Triple active-notice path from `triple-health-watch`
+
+### 4. Tight overall timeout
+
+Terminal 1:
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=10s -step-timeout=1s
+```
+
+Terminal 2:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000
+```
+
+This is mainly for comparing server logs with a tighter overall graceful 
shutdown budget.
+
+### 5. Compare long and short connections
+
+Long connection:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000
+```
+
+Short connection:
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-short=true
+```
+
+Long connections are the more relevant path for active graceful notices.
+
+## Integration Test
+
+This sample is wired into the root integration test flow:
+
+```bash
+./integrate_test.sh graceful_shutdown
+```
+
+The script starts the Triple server, runs the client in the background, waits 
until at least one request succeeds, and then sends an interrupt signal to 
trigger graceful shutdown.
+
+Before the client exits, it must observe:
+
+- at least one successful request
+- at least one failed request during shutdown
+
+If those expectations are not met, the client panics so CI fails immediately.
+
+## Practical Notes
+
+- Triple is the intended protocol for manual verification in this sample.
+- This sample is intentionally Triple-only so it focuses on the active notice 
path implemented in the current graceful shutdown flow.
+- Because this sample has no registry, the "unregister from registry" phase is 
only part of the core implementation flow, not something you can fully observe 
here.
diff --git a/graceful_shutdown/README_CN.md b/graceful_shutdown/README_CN.md
new file mode 100644
index 00000000..b84c902f
--- /dev/null
+++ b/graceful_shutdown/README_CN.md
@@ -0,0 +1,210 @@
+# 优雅停机示例
+
+[English](README.md) | 中文
+
+该示例用于验证 `dubbo-go` 中 Triple 协议的优雅停机流程。
+
+它主要覆盖以下行为:
+
+- 长连接消费者的主动通知
+- 消费端在停机期间的被动关闭表现
+- Provider 停机时对进行中请求的等待与排空
+- `timeout`、`step-timeout`、`consumer-update-wait` 和 `offline-window` 等参数的影响
+
+该示例**不包含注册中心**。因此你可以验证协议层的主动通知和请求排空行为,但不能直接观察“从注册中心摘除并传播”的完整链路。
+
+## 前置条件
+
+该示例使用仓库根目录下的 `go.mod`。
+
+请在本地 `dubbo-go-samples` 仓库根目录执行命令:
+
+```bash
+cd /path/to/dubbo-go-samples
+```
+
+## 快速开始
+
+在一个终端中启动服务端:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=60s -step-timeout=5s 
-delay=2s
+```
+
+在另一个终端中启动客户端:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=3 -interval=300ms -request-timeout=6s
+```
+
+然后在服务端终端按下 `Ctrl+C`。
+
+预期现象:
+
+- 已经在执行中的请求仍有机会完成
+- 停机开始后,新请求会逐渐失败
+- 服务端日志会按顺序输出优雅停机阶段
+
+## 重要地址格式
+
+进行直连调用时,`-addr` 必须带协议前缀。
+
+例如:
+
+- Triple:`tri://127.0.0.1:20000`
+
+如果只传 `127.0.0.1:20000`,在某些场景下可能会被错误解析。
+
+## 服务端参数
+
+`graceful_shutdown/go-server/cmd/main.go` 支持以下参数:
+
+- `-port=20000`
+- `-timeout=60s`
+- `-step-timeout=3s`
+- `-consumer-update-wait=3s`
+- `-offline-window=3s`
+- `-delay=0s`
+
+其中 `-delay` 会给每次请求增加固定处理延迟,用于观察停机时的在途请求排空效果。
+
+## 客户端参数
+
+`graceful_shutdown/go-client/cmd/main.go` 支持以下参数:
+
+- `-addr=tri://127.0.0.1:20000`
+- `-interval=200ms`
+- `-concurrency=1`
+- `-request-timeout=5s`
+- `-short=true|false`
+- `-name-prefix=hello`
+- `-max-requests=0`
+- `-min-successes=0`
+- `-min-failures=0`
+
+长连接验证时建议保持 `-short=false`。
+
+其中 `-max-requests`、`-min-successes`、`-min-failures` 
主要用于自动化测试。如果客户端在退出前没有达到这些最小阈值,会直接 `panic`,从而让集成测试失败。
+
+## 推荐场景
+
+### 1. 长连接下的 Triple 主动通知
+
+终端 1:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=60s
+```
+
+终端 2:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=1 -interval=200ms
+```
+
+然后在服务端终端按下 `Ctrl+C`。
+
+可观察点:
+
+- 服务端日志会打印优雅停机各阶段
+- 客户端在停机开始后不久会出现失败请求
+- 长连接会收到主动通知,而不是仅在进程退出后断开
+
+### 2. 在途请求排空
+
+终端 1:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -delay=2s -step-timeout=5s
+```
+
+终端 2:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=3 -interval=300ms -request-timeout=6s
+```
+
+在请求尚未完成时于服务端终端按下 `Ctrl+C`。
+
+可观察点:
+
+- 已经开始执行的请求仍可能成功返回
+- 新请求会在停机阶段开始失败
+- 服务端会在等待预算耗尽或请求完成后退出
+
+### 3. 同时观察主动通知与请求排空
+
+缩短消费者更新等待时间,使停机更早开始拒绝新请求,同时保留已有请求排空窗口。
+
+终端 1:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -delay=2s -timeout=15s 
-step-timeout=2s -consumer-update-wait=0s
+```
+
+终端 2:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-concurrency=2 -interval=200ms -request-timeout=4s
+```
+
+然后在服务端终端按下 `Ctrl+C`。
+
+可观察点:
+
+- 服务端日志会打印完整优雅停机序列
+- 某些在途请求会在停机开始后继续完成
+- 更新更晚到达的新请求会更早失败
+- 客户端日志会体现 Triple 长连接的主动通知路径
+
+### 4. 收紧整体超时预算
+
+终端 1:
+
+```bash
+go run ./graceful_shutdown/go-server/cmd -timeout=10s -step-timeout=1s
+```
+
+终端 2:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000
+```
+
+该场景主要用于对比更紧整体优雅停机预算下的服务端日志表现。
+
+### 5. 对比长连接与短连接
+
+长连接:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000
+```
+
+短连接:
+
+```bash
+go run ./graceful_shutdown/go-client/cmd -addr=tri://127.0.0.1:20000 
-short=true
+```
+
+针对主动通知路径,长连接更有代表性。
+
+## 集成测试
+
+该示例已接入根目录脚本驱动的集成测试:
+
+```bash
+./integrate_test.sh graceful_shutdown
+```
+
+脚本会启动 Triple 服务端,后台运行客户端,在观察到至少一次成功请求后向服务端发送中断信号,并要求客户端在退出前同时观察到:
+
+- 至少一次成功请求
+- 至少一次停机期间的失败请求
+
+如果这些条件没有满足,客户端会直接 `panic`,从而使 CI 失败。
+
+## 补充说明
+
+- 该示例以 Triple 协议为主,用于聚焦当前优雅停机流程中的主动通知路径。
+- 因为没有注册中心,这里只能覆盖协议层停机行为,不能完整覆盖注册中心摘除传播。
diff --git a/graceful_shutdown/go-client/cmd/main.go 
b/graceful_shutdown/go-client/cmd/main.go
new file mode 100644
index 00000000..d681424d
--- /dev/null
+++ b/graceful_shutdown/go-client/cmd/main.go
@@ -0,0 +1,158 @@
+/*
+ * 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 main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "sync"
+       "sync/atomic"
+       "time"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/client"
+       _ "dubbo.apache.org/dubbo-go/v3/imports"
+
+       "github.com/dubbogo/gost/log/logger"
+)
+
+import (
+       greet "github.com/apache/dubbo-go-samples/graceful_shutdown/proto"
+)
+
+func main() {
+       addr := flag.String("addr", "tri://127.0.0.1:20000", "server address")
+       interval := flag.Duration("interval", 200*time.Millisecond, "interval 
between requests for each worker")
+       shortConn := flag.Bool("short", false, "use short connection (create 
new client for each request)")
+       concurrency := flag.Int("concurrency", 1, "number of concurrent request 
loops")
+       requestTimeout := flag.Duration("request-timeout", 5*time.Second, 
"per-request timeout")
+       namePrefix := flag.String("name-prefix", "hello", "request name prefix")
+       maxRequests := flag.Int64("max-requests", 0, "maximum number of 
requests to issue across all workers, 0 means unlimited")
+       minSuccesses := flag.Int64("min-successes", 0, "minimum number of 
successful requests required before exit")
+       minFailures := flag.Int64("min-failures", 0, "minimum number of failed 
requests required before exit")
+       flag.Parse()
+
+       logger.Infof("Starting client, addr=%s short=%v concurrency=%d 
interval=%s request-timeout=%s",
+               *addr, *shortConn, *concurrency, interval.String(), 
requestTimeout.String())
+
+       var requestCounter atomic.Int64
+       var successCount atomic.Int64
+       var failureCount atomic.Int64
+       var wg sync.WaitGroup
+       for workerID := 1; workerID <= *concurrency; workerID++ {
+               wg.Add(1)
+               go func(id int) {
+                       defer wg.Done()
+                       runWorker(id, *addr, *interval, *shortConn, 
*requestTimeout, *namePrefix, *maxRequests, &requestCounter, &successCount, 
&failureCount)
+               }(workerID)
+       }
+
+       wg.Wait()
+
+       if *maxRequests > 0 {
+               successes := successCount.Load()
+               failures := failureCount.Load()
+               validateRequestSummary(*minSuccesses, successes, *minFailures, 
failures)
+               logger.Infof("Client finished, requests=%d successes=%d 
failures=%d", requestCounter.Load(), successes, failures)
+       }
+}
+
+func runWorker(workerID int, addr string, interval time.Duration, shortConn 
bool, requestTimeout time.Duration, namePrefix string, maxRequests int64, 
requestCounter, successCount, failureCount *atomic.Int64) {
+       var svc greet.GreetService
+       var err error
+
+       if !shortConn {
+               _, svc, err = newGreetClient(addr)
+               if err != nil {
+                       logger.Errorf("Worker %d failed to create long 
connection client: %v", workerID, err)
+               }
+       }
+
+       for {
+               requestID, ok := nextRequestID(requestCounter, maxRequests)
+               if !ok {
+                       return
+               }
+
+               if shortConn {
+                       _, svc, err = newGreetClient(addr)
+               }
+               if err != nil {
+                       failureCount.Add(1)
+                       logger.Errorf("Worker %d failed to prepare client: %v", 
workerID, err)
+                       time.Sleep(interval)
+                       continue
+               }
+
+               name := fmt.Sprintf("%s-%d", namePrefix, requestID)
+               ctx, cancel := context.WithTimeout(context.Background(), 
requestTimeout)
+               resp, callErr := svc.Greet(ctx, &greet.GreetRequest{Name: name})
+               cancel()
+
+               if callErr != nil {
+                       failureCount.Add(1)
+                       logger.Errorf("Worker %d request %d failed: %v", 
workerID, requestID, callErr)
+               } else {
+                       successCount.Add(1)
+                       logger.Infof("Worker %d request %d succeeded: %s", 
workerID, requestID, resp.Greeting)
+               }
+
+               time.Sleep(interval)
+       }
+}
+
+func newGreetClient(addr string) (*client.Client, greet.GreetService, error) {
+       cli, err := client.NewClient(client.WithClientURL(addr))
+       if err != nil {
+               return nil, nil, err
+       }
+
+       svc, err := greet.NewGreetService(cli)
+       if err != nil {
+               return nil, nil, err
+       }
+       return cli, svc, nil
+}
+
+func nextRequestID(counter *atomic.Int64, maxRequests int64) (int64, bool) {
+       if maxRequests == 0 {
+               return counter.Add(1), true
+       }
+
+       for {
+               current := counter.Load()
+               if current >= maxRequests {
+                       return 0, false
+               }
+               next := current + 1
+               if counter.CompareAndSwap(current, next) {
+                       return next, true
+               }
+       }
+}
+
+func validateRequestSummary(minSuccesses, successes, minFailures, failures 
int64) {
+       if successes < minSuccesses {
+               panic(fmt.Sprintf("expected at least %d successful requests, 
got %d", minSuccesses, successes))
+       }
+       if failures < minFailures {
+               panic(fmt.Sprintf("expected at least %d failed requests, got 
%d", minFailures, failures))
+       }
+}
diff --git a/graceful_shutdown/go-server/cmd/main.go 
b/graceful_shutdown/go-server/cmd/main.go
new file mode 100644
index 00000000..8867f507
--- /dev/null
+++ b/graceful_shutdown/go-server/cmd/main.go
@@ -0,0 +1,107 @@
+/*
+ * 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 main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "time"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/graceful_shutdown"
+       _ "dubbo.apache.org/dubbo-go/v3/imports"
+       "dubbo.apache.org/dubbo-go/v3/protocol"
+       "dubbo.apache.org/dubbo-go/v3/server"
+
+       "github.com/dubbogo/gost/log/logger"
+)
+
+import (
+       greet "github.com/apache/dubbo-go-samples/graceful_shutdown/proto"
+)
+
+type GreetProvider struct {
+       fixedDelay time.Duration
+}
+
+func (p *GreetProvider) Greet(ctx context.Context, req *greet.GreetRequest) 
(*greet.GreetResponse, error) {
+       start := time.Now()
+       logger.Infof("Handling greet request, name=%s delay=%s", req.Name, 
p.fixedDelay)
+
+       if p.fixedDelay > 0 {
+               timer := time.NewTimer(p.fixedDelay)
+               defer timer.Stop()
+
+               select {
+               case <-timer.C:
+               case <-ctx.Done():
+                       logger.Warnf("Greet request canceled before completion, 
name=%s err=%v", req.Name, ctx.Err())
+                       return nil, ctx.Err()
+               }
+       }
+
+       resp := &greet.GreetResponse{
+               Greeting: fmt.Sprintf("%s response after %s", req.Name, 
time.Since(start).Truncate(time.Millisecond)),
+       }
+       logger.Infof("Greet request finished, name=%s cost=%s", req.Name, 
time.Since(start).Truncate(time.Millisecond))
+       return resp, nil
+}
+
+func main() {
+       port := flag.Int("port", 20000, "triple listen port")
+       timeout := flag.Duration("timeout", 60*time.Second, "overall graceful 
shutdown timeout budget")
+       stepTimeout := flag.Duration("step-timeout", 3*time.Second, "timeout 
for waiting provider and consumer in-flight requests")
+       consumerUpdateWait := flag.Duration("consumer-update-wait", 
3*time.Second, "time to wait for consumers to observe instance changes")
+       offlineWindow := flag.Duration("offline-window", 3*time.Second, "time 
window for observing late requests after offline")
+       requestDelay := flag.Duration("delay", 0, "artificial delay added to 
each greet request")
+       flag.Parse()
+
+       graceful_shutdown.Init(
+               graceful_shutdown.WithTimeout(*timeout),
+               graceful_shutdown.WithStepTimeout(*stepTimeout),
+               
graceful_shutdown.WithConsumerUpdateWaitTime(*consumerUpdateWait),
+               
graceful_shutdown.WithOfflineRequestWindowTimeout(*offlineWindow),
+       )
+       logger.Infof("Graceful shutdown initialized, timeout=%s step-timeout=%s 
consumer-update-wait=%s offline-window=%s request-delay=%s",
+               timeout.String(), stepTimeout.String(), 
consumerUpdateWait.String(), offlineWindow.String(), requestDelay.String())
+
+       srv, err := server.NewServer(
+               server.WithServerProtocol(
+                       protocol.WithProtocol("tri"),
+                       protocol.WithPort(*port),
+                       protocol.WithID("tri"),
+               ),
+       )
+       if err != nil {
+               logger.Fatalf("failed to create server: %v", err)
+       }
+       logger.Infof("Exposing Triple on port %d", *port)
+
+       provider := &GreetProvider{fixedDelay: *requestDelay}
+       if err := greet.RegisterGreetServiceHandler(srv, provider); err != nil {
+               logger.Fatalf("failed to register greet service handler: %v", 
err)
+       }
+
+       logger.Info("Triple server started, press Ctrl+C to trigger graceful 
shutdown")
+
+       if err := srv.Serve(); err != nil {
+               logger.Fatalf("failed to serve: %v", err)
+       }
+}
diff --git a/graceful_shutdown/proto/greet.pb.go 
b/graceful_shutdown/proto/greet.pb.go
new file mode 100644
index 00000000..2e3a8136
--- /dev/null
+++ b/graceful_shutdown/proto/greet.pb.go
@@ -0,0 +1,190 @@
+//
+// 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 protoc-gen-go. DO NOT EDIT.
+// versions:
+//     protoc-gen-go v1.36.6
+//     protoc        v7.34.1
+// source: greet.proto
+
+package greet
+
+import (
+       protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+       protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+       reflect "reflect"
+       sync "sync"
+       unsafe "unsafe"
+)
+
+const (
+       // Verify that this generated code is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+       // Verify that runtime/protoimpl is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GreetRequest struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Name          string                 
`protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *GreetRequest) Reset() {
+       *x = GreetRequest{}
+       mi := &file_greet_proto_msgTypes[0]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *GreetRequest) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GreetRequest) ProtoMessage() {}
+
+func (x *GreetRequest) ProtoReflect() protoreflect.Message {
+       mi := &file_greet_proto_msgTypes[0]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use GreetRequest.ProtoReflect.Descriptor instead.
+func (*GreetRequest) Descriptor() ([]byte, []int) {
+       return file_greet_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GreetRequest) GetName() string {
+       if x != nil {
+               return x.Name
+       }
+       return ""
+}
+
+type GreetResponse struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Greeting      string                 
`protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *GreetResponse) Reset() {
+       *x = GreetResponse{}
+       mi := &file_greet_proto_msgTypes[1]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *GreetResponse) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GreetResponse) ProtoMessage() {}
+
+func (x *GreetResponse) ProtoReflect() protoreflect.Message {
+       mi := &file_greet_proto_msgTypes[1]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use GreetResponse.ProtoReflect.Descriptor instead.
+func (*GreetResponse) Descriptor() ([]byte, []int) {
+       return file_greet_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GreetResponse) GetGreeting() string {
+       if x != nil {
+               return x.Greeting
+       }
+       return ""
+}
+
+var File_greet_proto protoreflect.FileDescriptor
+
+const file_greet_proto_rawDesc = "" +
+       "\n" +
+       "\vgreet.proto\x12\x05greet\"\"\n" +
+       "\fGreetRequest\x12\x12\n" +
+       "\x04name\x18\x01 \x01(\tR\x04name\"+\n" +
+       "\rGreetResponse\x12\x1a\n" +
+       "\bgreeting\x18\x01 \x01(\tR\bgreeting2B\n" +
+       "\fGreetService\x122\n" +
+       "\x05Greet\x12\x13.greet.GreetRequest\x1a\x14.greet.GreetResponseBK\n" +
+       
"\x05greetP\[email protected]/apache/dubbo-go-samples/graceful_shutdown/proto;greetb\x06proto3"
+
+var (
+       file_greet_proto_rawDescOnce sync.Once
+       file_greet_proto_rawDescData []byte
+)
+
+func file_greet_proto_rawDescGZIP() []byte {
+       file_greet_proto_rawDescOnce.Do(func() {
+               file_greet_proto_rawDescData = 
protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_greet_proto_rawDesc),
 len(file_greet_proto_rawDesc)))
+       })
+       return file_greet_proto_rawDescData
+}
+
+var file_greet_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_greet_proto_goTypes = []any{
+       (*GreetRequest)(nil),  // 0: greet.GreetRequest
+       (*GreetResponse)(nil), // 1: greet.GreetResponse
+}
+var file_greet_proto_depIdxs = []int32{
+       0, // 0: greet.GreetService.Greet:input_type -> greet.GreetRequest
+       1, // 1: greet.GreetService.Greet:output_type -> greet.GreetResponse
+       1, // [1:2] is the sub-list for method output_type
+       0, // [0:1] is the sub-list for method input_type
+       0, // [0:0] is the sub-list for extension type_name
+       0, // [0:0] is the sub-list for extension extendee
+       0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_greet_proto_init() }
+func file_greet_proto_init() {
+       if File_greet_proto != nil {
+               return
+       }
+       type x struct{}
+       out := protoimpl.TypeBuilder{
+               File: protoimpl.DescBuilder{
+                       GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+                       RawDescriptor: 
unsafe.Slice(unsafe.StringData(file_greet_proto_rawDesc), 
len(file_greet_proto_rawDesc)),
+                       NumEnums:      0,
+                       NumMessages:   2,
+                       NumExtensions: 0,
+                       NumServices:   1,
+               },
+               GoTypes:           file_greet_proto_goTypes,
+               DependencyIndexes: file_greet_proto_depIdxs,
+               MessageInfos:      file_greet_proto_msgTypes,
+       }.Build()
+       File_greet_proto = out.File
+       file_greet_proto_goTypes = nil
+       file_greet_proto_depIdxs = nil
+}
diff --git a/graceful_shutdown/proto/greet.proto 
b/graceful_shutdown/proto/greet.proto
new file mode 100644
index 00000000..42ef43fc
--- /dev/null
+++ b/graceful_shutdown/proto/greet.proto
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package greet;
+
+option go_package = 
"github.com/apache/dubbo-go-samples/graceful_shutdown/proto;greet";
+option java_package = "greet";
+option java_multiple_files = true;
+
+message GreetRequest {
+  string name = 1;
+}
+
+message GreetResponse {
+  string greeting = 1;
+}
+
+service GreetService {
+  rpc Greet(GreetRequest) returns (GreetResponse);
+}
diff --git a/graceful_shutdown/proto/greet.triple.go 
b/graceful_shutdown/proto/greet.triple.go
new file mode 100644
index 00000000..48bc7495
--- /dev/null
+++ b/graceful_shutdown/proto/greet.triple.go
@@ -0,0 +1,122 @@
+// Code generated by protoc-gen-triple. DO NOT EDIT.
+//
+// Source: graceful_shutdown/proto/greet.proto
+package greet
+
+import (
+       "context"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3"
+       "dubbo.apache.org/dubbo-go/v3/client"
+       "dubbo.apache.org/dubbo-go/v3/common"
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
+       "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
+       "dubbo.apache.org/dubbo-go/v3/server"
+)
+
+// This is a compile-time assertion to ensure that this generated file and the 
Triple package
+// are compatible. If you get a compiler error that this constant is not 
defined, this code was
+// generated with a version of Triple newer than the one compiled into your 
binary. You can fix the
+// problem by either regenerating this code with an older version of Triple or 
updating the Triple
+// version compiled into your binary.
+const _ = triple_protocol.IsAtLeastVersion0_1_0
+
+const (
+       // GreetServiceName is the fully-qualified name of the GreetService 
service.
+       GreetServiceName = "greet.GreetService"
+)
+
+// These constants are the fully-qualified names of the RPCs defined in this 
package. They're
+// exposed at runtime as procedure and as the final two segments of the HTTP 
route.
+//
+// Note that these are different from the fully-qualified method names used by
+// google.golang.org/protobuf/reflect/protoreflect. To convert from these 
constants to
+// reflection-formatted method names, remove the leading slash and convert the 
remaining slash to a
+// period.
+const (
+       // GreetServiceGreetProcedure is the fully-qualified name of the 
GreetService's Greet RPC.
+       GreetServiceGreetProcedure = "/greet.GreetService/Greet"
+)
+
+var (
+       _ GreetService = (*GreetServiceImpl)(nil)
+)
+
+// GreetService is a client for the greet.GreetService service.
+type GreetService interface {
+       Greet(ctx context.Context, req *GreetRequest, opts 
...client.CallOption) (*GreetResponse, error)
+}
+
+// NewGreetService constructs a client for the greet.GreetService service.
+func NewGreetService(cli *client.Client, opts ...client.ReferenceOption) 
(GreetService, error) {
+       conn, err := cli.DialWithInfo("greet.GreetService", 
&GreetService_ClientInfo, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return &GreetServiceImpl{
+               conn: conn,
+       }, nil
+}
+
+func SetConsumerGreetService(srv common.RPCService) {
+       dubbo.SetConsumerServiceWithInfo(srv, &GreetService_ClientInfo)
+}
+
+// GreetServiceImpl implements GreetService.
+type GreetServiceImpl struct {
+       conn *client.Connection
+}
+
+func (c *GreetServiceImpl) Greet(ctx context.Context, req *GreetRequest, opts 
...client.CallOption) (*GreetResponse, error) {
+       resp := new(GreetResponse)
+       if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "Greet", 
opts...); err != nil {
+               return nil, err
+       }
+       return resp, nil
+}
+
+var GreetService_ClientInfo = client.ClientInfo{
+       InterfaceName: "greet.GreetService",
+       MethodNames:   []string{"Greet"},
+       ConnectionInjectFunc: func(dubboCliRaw interface{}, conn 
*client.Connection) {
+               dubboCli := dubboCliRaw.(*GreetServiceImpl)
+               dubboCli.conn = conn
+       },
+}
+
+// GreetServiceHandler is an implementation of the greet.GreetService service.
+type GreetServiceHandler interface {
+       Greet(context.Context, *GreetRequest) (*GreetResponse, error)
+}
+
+func RegisterGreetServiceHandler(srv *server.Server, hdlr GreetServiceHandler, 
opts ...server.ServiceOption) error {
+       return srv.Register(hdlr, &GreetService_ServiceInfo, opts...)
+}
+
+func SetProviderGreetService(srv common.RPCService) {
+       dubbo.SetProviderServiceWithInfo(srv, &GreetService_ServiceInfo)
+}
+
+var GreetService_ServiceInfo = server.ServiceInfo{
+       InterfaceName: "greet.GreetService",
+       ServiceType:   (*GreetServiceHandler)(nil),
+       Methods: []server.MethodInfo{
+               {
+                       Name: "Greet",
+                       Type: constant.CallUnary,
+                       ReqInitFunc: func() interface{} {
+                               return new(GreetRequest)
+                       },
+                       MethodFunc: func(ctx context.Context, args 
[]interface{}, handler interface{}) (interface{}, error) {
+                               req := args[0].(*GreetRequest)
+                               res, err := 
handler.(GreetServiceHandler).Greet(ctx, req)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               return triple_protocol.NewResponse(res), nil
+                       },
+               },
+       },
+}
diff --git a/integrate_test.sh b/integrate_test.sh
index 3a507488..81ef2965 100755
--- a/integrate_test.sh
+++ b/integrate_test.sh
@@ -65,12 +65,18 @@ kill_if_running() {
 }
 
 cleanup() {
+  local server_pid=""
   local aux_pid
   for aux_pid in "${GO_AUX_PIDS[@]:-}"; do
     kill_if_running "$aux_pid"
   done
 
   kill_if_running "$JAVA_SERVER_PID"
+  if [ -f "$PID_FILE" ]; then
+    server_pid="$(cat "$PID_FILE" 2>/dev/null || true)"
+    kill_if_running "$server_pid"
+    rm -f "$PID_FILE"
+  fi
   run_make_target stop >/dev/null 2>&1 || true
 }
 trap cleanup EXIT
@@ -97,7 +103,65 @@ wait_for_tcp_port() {
   local elapsed=0
 
   while [ "$elapsed" -lt "$timeout_seconds" ]; do
-    if timeout 1 bash -c "cat < /dev/null > /dev/tcp/$host/$port" >/dev/null 
2>&1; then
+    if python3 - "$host" "$port" <<'PY' >/dev/null 2>&1
+import socket
+import sys
+
+host = sys.argv[1]
+port = int(sys.argv[2])
+
+family = socket.AF_UNSPEC
+type_ = socket.SOCK_STREAM
+
+for af, socktype, proto, _, sockaddr in socket.getaddrinfo(host, port, family, 
type_):
+    sock = None
+    try:
+        sock = socket.socket(af, socktype, proto)
+        sock.settimeout(1.0)
+        sock.connect(sockaddr)
+        sys.exit(0)
+    except OSError:
+        continue
+    finally:
+        if sock is not None:
+            sock.close()
+
+sys.exit(1)
+PY
+    then
+      return 0
+    fi
+    sleep 1
+    elapsed=$((elapsed + 1))
+  done
+
+  return 1
+}
+
+wait_for_process_exit() {
+  local pid="$1"
+  local timeout_seconds="$2"
+  local elapsed=0
+
+  while kill -0 "$pid" 2>/dev/null; do
+    if [ "$elapsed" -ge "$timeout_seconds" ]; then
+      return 1
+    fi
+    sleep 1
+    elapsed=$((elapsed + 1))
+  done
+
+  return 0
+}
+
+wait_for_log_pattern() {
+  local log_file="$1"
+  local pattern="$2"
+  local timeout_seconds="$3"
+  local elapsed=0
+
+  while [ "$elapsed" -lt "$timeout_seconds" ]; do
+    if [ -f "$log_file" ] && grep -q "$pattern" "$log_file"; then
       return 0
     fi
     sleep 1
@@ -275,12 +339,113 @@ start_java_server_if_present() {
   return 0
 }
 
+run_graceful_shutdown_sample() {
+  local client_log="/tmp/.${PROJECT_NAME}.go-client.log"
+  local server_pid=""
+  local client_pid=""
+  local server_bin="/tmp/.${PROJECT_NAME}.go-server.bin"
+  local client_bin="/tmp/.${PROJECT_NAME}.go-client.bin"
+
+  if command -v lsof >/dev/null 2>&1; then
+    lsof -tiTCP:20000 -sTCP:LISTEN | xargs -r kill -9 || true
+  fi
+
+  echo "Building graceful_shutdown Go server..."
+  (
+    cd "$P_DIR"
+    go build -o "$server_bin" ./go-server/cmd
+  )
+
+  echo "Building graceful_shutdown Go client..."
+  (
+    cd "$P_DIR"
+    go build -o "$client_bin" ./go-client/cmd
+  )
+
+  echo "Starting graceful_shutdown Go server..."
+  (
+    cd "$P_DIR"
+    exec "$server_bin" -timeout=15s -step-timeout=2s -consumer-update-wait=0s 
-delay=2s
+  ) >"$GO_SERVER_LOG" 2>&1 &
+  server_pid="$!"
+  echo "$server_pid" >"$PID_FILE"
+
+  if ! wait_for_tcp_port "127.0.0.1" "20000" 30; then
+    echo "graceful_shutdown server did not become ready on 127.0.0.1:20000"
+    cat "$GO_SERVER_LOG" || true
+    return 1
+  fi
+
+  if ! kill -0 "$server_pid" 2>/dev/null; then
+    echo "graceful_shutdown server exited unexpectedly before client start"
+    cat "$GO_SERVER_LOG" || true
+    return 1
+  fi
+
+  echo "Running graceful_shutdown Go client..."
+  (
+    cd "$P_DIR"
+    exec "$client_bin" \
+      -addr=tri://127.0.0.1:20000 \
+      -concurrency=2 \
+      -interval=200ms \
+      -short \
+      -request-timeout=6s \
+      -max-requests=12 \
+      -min-successes=1 \
+      -min-failures=1 \
+      -name-prefix=integration
+  ) >"$client_log" 2>&1 &
+  client_pid="$!"
+
+  if ! wait_for_log_pattern "$client_log" "succeeded" 30; then
+    echo "graceful_shutdown client did not observe a successful request before 
shutdown"
+    cat "$client_log" || true
+    cat "$GO_SERVER_LOG" || true
+    return 1
+  fi
+
+  echo "Triggering graceful shutdown..."
+  kill -INT "$server_pid" 2>/dev/null || true
+
+  if ! wait "$client_pid"; then
+    echo "graceful_shutdown client exited with failure"
+    cat "$client_log" || true
+    cat "$GO_SERVER_LOG" || true
+    return 1
+  fi
+
+  if ! wait_for_process_exit "$server_pid" 30; then
+    echo "graceful_shutdown server did not exit within 30s after SIGINT"
+    cat "$GO_SERVER_LOG" || true
+    return 1
+  fi
+
+  wait "$server_pid" 2>/dev/null || true
+
+  if ! grep -q "failed" "$client_log"; then
+    echo "graceful_shutdown client did not observe request failures during 
shutdown"
+    cat "$client_log" || true
+    return 1
+  fi
+
+  echo "graceful_shutdown integration completed"
+}
+
 main() {
   echo "=========================================="
   echo "Starting sample flow for: $SAMPLE"
   echo "Sample directory: $P_DIR"
   echo "=========================================="
 
+  if [ "$SAMPLE" = "graceful_shutdown" ]; then
+    run_graceful_shutdown_sample
+    echo "=========================================="
+    echo "Sample flow completed for: $SAMPLE"
+    echo "=========================================="
+    return 0
+  fi
+
   start_go_server
   start_aux_go_servers
 
diff --git a/start_integrate_test.sh b/start_integrate_test.sh
index df11ea29..7706ea76 100755
--- a/start_integrate_test.sh
+++ b/start_integrate_test.sh
@@ -47,6 +47,9 @@ array+=("timeout")
 # healthcheck
 array+=("healthcheck")
 
+# graceful shutdown
+array+=("graceful_shutdown")
+
 # streaming
 array+=("streaming")
 


Reply via email to