feat: expose transaction failure stages and conservative outcomes
This commit is contained in:
24
examples/transactions/README.md
Normal file
24
examples/transactions/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 事务失败与结果不确定
|
||||
|
||||
在框架根目录运行:
|
||||
|
||||
```sh
|
||||
go run ./examples/transactions
|
||||
```
|
||||
|
||||
示例使用确定性的演示 SQL 驱动,没有真实数据库、上传文件或外部服务。业务调用仍走真实 `database/sql` 和 `database.InTx`;`demoDriver.go` 仅用来注入数据库操作成功后确认丢失的情况,项目继续使用自己的正常驱动。
|
||||
|
||||
```text
|
||||
success: committed, stored=true
|
||||
beginFailure: stage=begin outcome=notStarted stored=false cleanup=true
|
||||
workFailure: stage=work outcome=rolledBack stored=false cleanup=true
|
||||
rollbackFailure: stage=work outcome=unknown stored=false cleanup=false
|
||||
commitRejected: stage=commit outcome=unknown stored=false cleanup=false
|
||||
commitAcknowledgementLost: stage=commit outcome=unknown stored=true cleanup=false
|
||||
```
|
||||
|
||||
最后两个场景收到一样的提交错误,但一个没有保存、另一个已经保存。调用方不能从提交错误推断数据库没记录,因此示例只在已知未执行回调或已回滚时考虑清理,不确定时保留。`cleanup` 只是演示业务决策,没有执行任何删除;实际业务仍需考虑文件是否只属于本次操作、其他事务或外部副作用。
|
||||
|
||||
`errors.As` 取得 TxError 的阶段/结果,`errors.Is` 仍找到原始演示错误。回滚失败有独立原因;TxError 不规定业务码、HTTP 状态或清理策略。
|
||||
|
||||
完整接口、自动回滚的观察边界和兼容注意见 [database](../../database/README.md#事务阶段与结果)。真实 MySQL 和故障回归见 [验收记录](../../reports/transactionAcceptance.md)。
|
||||
48
examples/transactions/demoDriver.go
Normal file
48
examples/transactions/demoDriver.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// This driver models an acknowledgement lost AFTER persistence. Applications
|
||||
// use their normal SQL driver; this is solely a runnable fault demonstration.
|
||||
type demoConnector struct {
|
||||
beginErr, commitErr, rollbackErr error
|
||||
persistBeforeError bool
|
||||
stored atomic.Bool
|
||||
}
|
||||
|
||||
func (d *demoConnector) Connect(context.Context) (driver.Conn, error) {
|
||||
return &demoConn{owner: d}, nil
|
||||
}
|
||||
func (d *demoConnector) Driver() driver.Driver { return d }
|
||||
func (d *demoConnector) Open(string) (driver.Conn, error) { return d.Connect(context.Background()) }
|
||||
|
||||
type demoConn struct{ owner *demoConnector }
|
||||
|
||||
func (c *demoConn) Prepare(string) (driver.Stmt, error) {
|
||||
return nil, errors.New("not implemented by demo")
|
||||
}
|
||||
func (c *demoConn) Close() error { return nil }
|
||||
func (c *demoConn) Begin() (driver.Tx, error) {
|
||||
if c.owner.beginErr != nil {
|
||||
return nil, c.owner.beginErr
|
||||
}
|
||||
return &demoTx{owner: c.owner}, nil
|
||||
}
|
||||
func (c *demoConn) ExecContext(context.Context, string, []driver.NamedValue) (driver.Result, error) {
|
||||
return driver.RowsAffected(1), nil
|
||||
}
|
||||
|
||||
type demoTx struct{ owner *demoConnector }
|
||||
|
||||
func (tx *demoTx) Commit() error {
|
||||
if tx.owner.commitErr == nil || tx.owner.persistBeforeError {
|
||||
tx.owner.stored.Store(true)
|
||||
}
|
||||
return tx.owner.commitErr
|
||||
}
|
||||
func (tx *demoTx) Rollback() error { return tx.owner.rollbackErr }
|
||||
55
examples/transactions/main.go
Normal file
55
examples/transactions/main.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Run with go run ./examples/transactions. Uses a deterministic demonstration
|
||||
// driver, no external database and no actual uploaded files.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
failure := errors.New("demonstration failure")
|
||||
for _, name := range []string{"success", "beginFailure", "workFailure", "rollbackFailure", "commitRejected", "commitAcknowledgementLost"} {
|
||||
driver := &demoConnector{}
|
||||
var workErr error
|
||||
switch name {
|
||||
case "beginFailure":
|
||||
driver.beginErr = failure
|
||||
case "workFailure":
|
||||
workErr = failure
|
||||
case "rollbackFailure":
|
||||
workErr, driver.rollbackErr = failure, failure
|
||||
case "commitRejected":
|
||||
driver.commitErr = failure
|
||||
case "commitAcknowledgementLost":
|
||||
driver.commitErr, driver.persistBeforeError = failure, true
|
||||
}
|
||||
pool := &database.DB{DB: sql.OpenDB(driver)}
|
||||
err := pool.InTx(context.Background(), nil, func(tx *sql.Tx) error {
|
||||
if _, err := tx.ExecContext(context.Background(), "INSERT demonstration record"); err != nil {
|
||||
return err
|
||||
}
|
||||
return workErr
|
||||
})
|
||||
_ = pool.Close()
|
||||
if err == nil {
|
||||
fmt.Printf("%s: committed, stored=%t\n", name, driver.stored.Load())
|
||||
continue
|
||||
}
|
||||
var txErr *database.TxError
|
||||
if !errors.As(err, &txErr) || !errors.Is(err, failure) {
|
||||
panic("transaction phase or original cause lost")
|
||||
}
|
||||
// A project decides whether cleanup is appropriate for its own operation.
|
||||
// Unknown outcomes must not be treated as evidence that no record exists.
|
||||
cleanup := txErr.Outcome == database.TxOutcomeNotStarted || txErr.Outcome == database.TxOutcomeRolledBack
|
||||
if cleanup && driver.stored.Load() {
|
||||
panic("unsafe cleanup decision")
|
||||
}
|
||||
fmt.Printf("%s: stage=%s outcome=%s stored=%t cleanup=%t\n", name, txErr.Stage, txErr.Outcome, driver.stored.Load(), cleanup)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user