内容简介:最近使用如果起初我以为是 sqlmock 的 bug,但是追踪代码时发现,sqlmock 的 assertion 功能是正常的,问题出在
最近使用 sqlmock 编写单元测试时遇到一个问题。现有这样的代码:
defer func() { if err != nil { err = tx.Rollback() ... } } err = tx.Commit()
如果 tx.Commit()
失败了,那么 Rollback
的 mock assertion 不会被触发。但跟踪代码时我看到 tx.Rollback()
路径确实会被执行到的。
起初我以为是 sqlmock 的 bug,但是追踪代码时发现,sqlmock 的 assertion 功能是正常的,问题出在 tx.Rollback()
确实没有调用 sqlmock 提供的 mock driver 的 Rollback
方法。
Go 的 Tx.Rollback()
方法实现如下:
// Rollback aborts the transaction. func (tx *Tx) Rollback() error { return tx.rollback(false) }
对应的 rollback
方法:
// rollback aborts the transaction and optionally forces the pool to discard // the connection. func (tx *Tx) rollback(discardConn bool) error { if !atomic.CompareAndSwapInt32(&tx.done, 0, 1) { return ErrTxDone } var err error withLock(tx.dc, func() { err = tx.txi.Rollback() }) if err != driver.ErrBadConn { tx.closePrepared() } if discardConn { err = driver.ErrBadConn } tx.close(err) return err }
如果 tx.done
已经被设置了,会直接返回 ErrTxDone
错误,不会调用 tx.txi.Rollback()
这个 callback。
// ErrTxDone is returned by any operation that is performed on a transaction // that has already been committed or rolled back. var ErrTxDone = errors.New("sql: transaction has already been committed or rolled back")
显然 tx.done
是在 tx.Commit
的时候设置的。
func (tx *Tx) Commit() error { // Check context first to avoid transaction leak. // If put it behind tx.done CompareAndSwap statement, we can't ensure // the consistency between tx.done and the real COMMIT operation. select { default: case <-tx.ctx.Done(): if atomic.LoadInt32(&tx.done) == 1 { return ErrTxDone } return tx.ctx.Err() } if !atomic.CompareAndSwapInt32(&tx.done, 0, 1) { return ErrTxDone } var err error withLock(tx.dc, func() { err = tx.txi.Commit() }) if err != driver.ErrBadConn { tx.closePrepared() } tx.close(err) return err }
看了之后确实如此,而且 tx.done
是在调用 driver 的 tx.txi.Commit()
之前设置。也就是说,无论 Commit 的结果是否成功,只要执行了,再执行 Rollback 必然会返回 ErrTxDone
错误。(同样道理,如果已经 Rollback 了,再 Commit 也会报错,当然一般人也不会这么写代码)
这里隐含一个对 driver 背后的数据库的要求:如果 Commit 失败了,需要保证一定会 Rollback。具体可以拆成三点:
- 一旦 Commit 失败,需要对当前事务自动 Rollback
- 如果客户端失去响应,比如连接断了,需要 Rollback 当前的事务
- 只有在收到客户端确认 Commit 结果的响应后,才算是 Commit 结束。如果 Commit 成功,但是客户端没有确认,那么还是得 Rollback 当前事务。
(也许读者会觉得第 3 点是第 2 点下的一个特例,不过我觉得第 3 点要求数据库在设计协议的时候需要有个 Commit Response 类型的消息,所以值得拎出来单独分一类)
至于 driver 的用户,如果不能把 Commit 移到 defer 里面(比如函数底部存在耗时操作,需要提前 Commit 掉),可以把 Rollback 代码改成这样,避免无谓的报错:
defer func() { if err != nil { err = tx.Rollback() if err != sql.ErrTxDone { ... } } } err = tx.Commit()
或者:
defer func() { if err != nil { err = tx.Rollback() ... } } // 注意下面多了个冒号,这样 Commit 返回的 err 和 defer 里的 err // 是两个不同的错误,不会触发 Rollback err := tx.Commit() // 随便一提,如果 defer 的函数里引用了 err,后面就不要用 // err := func() 的写法,因为这种会覆盖掉前面被引用的 err。 // x, err := func() 这种写法是 OK 的,不会覆盖掉 err。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 快速失败机制 & 失败安全机制
- 通过不断地失败来避免失败,携程混沌工程实践
- 快速失败(fail-fast)和安全失败(fail-safe)
- greenplum 集群启动失败
- Nginx 失败重试机制
- greenplum集群启动失败问题分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
我是90后,我是创业家
腾讯互联网与社会研究院 / 华章图书 / 2015-1-1 / 48.00元
第1本揭秘17个90后精彩创业故事 他们是:脸萌创始人郭列、北大硕士卖米粉的张天一、微博《我只过1%的生活》短时间转发35万多次的伟大的安妮、备受争议的90后总裁余佳文、节操姐CEO陈桦……17位90后的创业家为你分享他们的创业故事!从这些90后孩子的经历中,还可以看到互联网带来的巨大好处,这又是这一代人的幸运。这些创业者有一个共同特点,即他们在做自己事业的时候,会经常遇到来自家庭和社会的阻......一起来看看 《我是90后,我是创业家》 这本书的介绍吧!