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")