Ethan-Xingyue opened a new issue, #1156:
URL: https://github.com/apache/incubator-seata-go/issues/1156
| Field | Value |
| --- | --- |
| Issue title | `[BUG] [RM][Getty/gRPC] Branch commit/rollback failures
return locally without sending a failure response to the TC` |
| Labels (exist in repo) | `bug`, `module/rm`, `remoting` |
| Suggested priority | P1 |
| Related | none known |
| Verification | Reproduced 2026-09-02 on master `3bf73586` with go1.24.3
darwin/arm64 (in-package test, `-gcflags=all=-l`) |
---
## 🚀 Go Version
go1.24.3 darwin/arm64
## 📦 Seata-go Version
master, commit 3bf73586af81db1bd428982c93d82000d80cb1c8 (fetched 2026-09-02)
## 💾 Operating System
macOS
## 📝 Bug Description
When the resource manager's `BranchCommit` / `BranchRollback` returns an
error, all four handler paths (Getty commit, Getty rollback, gRPC commit, gRPC
rollback) log the error and `return err` immediately. No response is sent to
the TC. The response-building code that follows still contains an `if err !=
nil { resultCode = ResultCodeFailed ... }` branch, but it is unreachable. The
Getty `OnMessage` and gRPC receive loops do not consume the error returned by
`Process` either.
As a result the TC never receives the retryable branch status; it only
notices the failure through its own RPC timeout, which extends lock holding
time and delays the retry.
Locations at 3bf73586 (the early `return err` lines):
- gRPC commit:
https://github.com/apache/incubator-seata-go/blob/3bf73586af81db1bd428982c93d82000d80cb1c8/pkg/remoting/processor/client/rm_branch_commit_processor.go#L74-L78
- Getty commit:
https://github.com/apache/incubator-seata-go/blob/3bf73586af81db1bd428982c93d82000d80cb1c8/pkg/remoting/processor/client/rm_branch_commit_processor.go#L131-L135
- gRPC rollback:
https://github.com/apache/incubator-seata-go/blob/3bf73586af81db1bd428982c93d82000d80cb1c8/pkg/remoting/processor/client/rm_branch_rollback_processor.go#L76-L80
- Getty rollback:
https://github.com/apache/incubator-seata-go/blob/3bf73586af81db1bd428982c93d82000d80cb1c8/pkg/remoting/processor/client/rm_branch_rollback_processor.go#L130-L134
## 🔄 Steps to Reproduce
1. Check out the commit:
```bash
git clone https://github.com/apache/incubator-seata-go.git
cd incubator-seata-go
git checkout 3bf73586af81db1bd428982c93d82000d80cb1c8
```
2. Save the following as
`pkg/remoting/processor/client/audit_branch_response_repro_test.go`. It
registers a resource manager whose commit/rollback always fail, patches
`SendAsyncResponse` on both remoting clients to count calls, and drives the
four handlers directly:
```go
package client
import (
"context"
"errors"
"reflect"
"sync"
"testing"
"github.com/agiledragon/gomonkey/v2"
"seata.apache.org/seata-go/v2/pkg/protocol/branch"
"seata.apache.org/seata-go/v2/pkg/protocol/message"
"seata.apache.org/seata-go/v2/pkg/remoting/getty"
grpcclient "seata.apache.org/seata-go/v2/pkg/remoting/grpc"
"seata.apache.org/seata-go/v2/pkg/remoting/grpc/pb"
"seata.apache.org/seata-go/v2/pkg/rm"
)
type auditFailingRM struct{ resources sync.Map }
func (*auditFailingRM) BranchCommit(context.Context, rm.BranchResource)
(branch.BranchStatus, error) {
return branch.BranchStatusPhasetwoCommitFailedRetryable,
errors.New("storage failed")
}
func (*auditFailingRM) BranchRollback(context.Context, rm.BranchResource)
(branch.BranchStatus, error) {
return branch.BranchStatusPhasetwoRollbackFailedRetryable,
errors.New("storage failed")
}
func (*auditFailingRM) BranchRegister(context.Context,
rm.BranchRegisterParam) (int64, error) {
return 0, nil
}
func (*auditFailingRM) BranchReport(context.Context, rm.BranchReportParam)
error { return nil }
func (*auditFailingRM) LockQuery(context.Context, rm.LockQueryParam) (bool,
error) { return false, nil }
func (*auditFailingRM) RegisterResource(rm.Resource) error
{ return nil }
func (*auditFailingRM) UnregisterResource(rm.Resource) error
{ return nil }
func (f *auditFailingRM) GetCachedResources() *sync.Map
{ return &f.resources }
func (*auditFailingRM) GetBranchType() branch.BranchType
{ return branch.BranchType(99) }
func TestAuditBranchFailureStillRepliesToTCBothProtocols(t *testing.T) {
rm.GetRmCacheInstance().RegisterResourceManager(&auditFailingRM{})
gettySent, grpcSent := 0, 0
gettyPatch := gomonkey.ApplyMethod(
reflect.TypeOf(getty.GetGettyRemotingClient()),
"SendAsyncResponse",
func(*getty.GettyRemotingClient, int32, interface{}) error {
gettySent++; return nil },
)
defer gettyPatch.Reset()
grpcPatch := gomonkey.ApplyMethod(
reflect.TypeOf(grpcclient.GetGrpcRemotingClient()),
"SendAsyncResponse",
func(*grpcclient.GrpcRemotingClient, int32, interface{}) error
{ grpcSent++; return nil },
)
defer grpcPatch.Reset()
bt := branch.BranchType(99)
commit := &rmBranchCommitProcessor{}
rollback := &rmBranchRollbackProcessor{}
_ = commit.handleGettyBranchCommit(context.Background(),
message.RpcMessage{
ID: 1, Body: message.BranchCommitRequest{
AbstractBranchEndRequest:
message.AbstractBranchEndRequest{BranchType: bt},
},
})
_ = rollback.handleGettyBranchRollback(context.Background(),
message.RpcMessage{
ID: 2, Body: message.BranchRollbackRequest{
AbstractBranchEndRequest:
message.AbstractBranchEndRequest{BranchType: bt},
},
})
abstract := &pb.AbstractBranchEndRequestProto{BranchType:
pb.BranchTypeProto(99)}
_ = commit.handleGrpcBranchCommit(context.Background(),
message.RpcMessage{
ID: 3, Body:
&pb.BranchCommitRequestProto{AbstractBranchEndRequest: abstract},
})
_ = rollback.handleGrpcBranchRollback(context.Background(),
message.RpcMessage{
ID: 4, Body:
&pb.BranchRollbackRequestProto{AbstractBranchEndRequest: abstract},
})
if gettySent != 2 || grpcSent != 2 {
t.Fatalf("failure responses: getty=%d grpc=%d, want 2 each",
gettySent, grpcSent)
}
}
```
3. Run (gomonkey needs inlining disabled):
```bash
go test -gcflags=all=-l \
-run '^TestAuditBranchFailureStillRepliesToTCBothProtocols$' \
-count=1 -v ./pkg/remoting/processor/client
```
Delete the temporary test file afterwards.
## ✅ Expected Behavior
All four failures produce a response: two via Getty, two via gRPC. Each
response carries the retryable branch status returned by the RM,
`ResultCodeFailed`, the xid/branch id and the error message (`storage failed`).
The TC does not have to wait for its RPC timeout.
## ❌ Actual Behavior
```text
=== RUN TestAuditBranchFailureStillRepliesToTCBothProtocols
ERROR: branch commit error: storage failed
ERROR: branch rollback error: storage failed
ERROR: branch commit error: storage failed
ERROR: branch rollback error: storage failed
audit_branch_response_repro_test.go:74: failure responses: getty=0
grpc=0, want 2 each
--- FAIL: TestAuditBranchFailureStillRepliesToTCBothProtocols (0.00s)
FAIL
FAIL seata.apache.org/seata-go/v2/pkg/remoting/processor/client 0.674s
FAIL
```
## 💡 Possible Solution
- Always build and send the response, for success and failure alike; on
failure use the RM-provided retryable status, `ResultCodeFailed` and the error
message.
- If sending the response fails, keep the business error as well (for
example `errors.Join(bizErr, sendErr)`), but never skip the send attempt.
- Make the success/failure status mapping a table shared by Getty and gRPC
so the transports only differ in encoding.
Acceptance criteria:
- [ ] The Getty/gRPC × commit/rollback test above passes.
- [ ] Result code, branch status, xid, branch id and message of the failure
response are asserted.
- [ ] When `SendAsyncResponse` itself fails, both the business error and the
send error are observable.
- [ ] An end-to-end test against a real TC shows the TC receives the failed
status immediately instead of timing out.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]