Helpful comments and good suggestion, thanks Leonel. -Ben On Tue, Dec 4, 2018 at 9:15 AM Leonel Quinteros <leonel.quinte...@gmail.com> wrote:
> Hi Ben, > > Sorry, I've misread that part. > Now that I got some sleep and reviewed your code a little bit more, it > looks like you got trapped by your own design decisions. > > First of all, variable shadowing is something every Go developer should be > able to handle gracefully, but I understand your concern about how easy > it's to miss that bug. Doesn't the linter alert you about that shadowing > process? Just curious about that. > > Then, the mistake, IMHO, is the use of the defer function and the custom > tx.Close method (is it custom made, right? I haven't found a reference for > that method in the sql.Tx type docs) that "saves" you from manually > handling tx.Commit() vs tx.Rollback() calls, which if you think, it gets > pretty well aligned to the error handling philosophy of Go. If you get an > error, please go ahead and explicitly handle that error. > > So, your DoTwoThings() function is actually doing 3 things, the 2 things > against the DB, but it's also handling the transaction. If you see these 2 > DB queries as a unit, you may want to put these in a method that takes a > sql.Tx object, executes queries in that context and return an error result > that will indicate if all of them were successful or not. Then you Commit > or Rollback the transaction in the caller, not in a deeper level. > So pretty much as your Transact() method, but I'm not a fan of the defer > use there, while I get how you managed to also catch panics there. > Out of your 2 proposals, I like this one better. > > But if you ask me, I'd recommend to avoid the defer use, which is the > thing that's actually causing the shadowing problem, and to enforce the > manually handling of the transactions directly related to error handling. > That's how I've been doing it and it's pretty clear and straightforward > when you read that code, the Rollbacks after the errors and the Commits at > the end of each function are just there, explicitly telling you what's > happening when some DB query fails. > > But again, it's a matter of opinion, design and personal preferences. > I just hope this helps for you to form an opinion. > > Best! > > > *Leonel Quinteros* > Director of Technology > Global Brigades <https://www.globalbrigades.org/> > > > > El lun., 3 de dic. de 2018 a la(s) 20:54, Ben Hoyt (benh...@gmail.com) > escribió: > >> Robert, here is code (actual working code this time) similar to what we >> have: https://play.golang.org/p/jUPqgnk6Ttk >> >> Leonel, yep, I understand that, which is why we have this problem. The >> thing is, this pattern makes it very easy to forget to always re-use the >> same error variable, especially in code that's a bit more complex with a >> bunch of nested ifs, etc. So we're trying to come up with a pattern that's >> hard or impossible to misuse, instead of easy to misuse. >> >> -Ben >> >> On Mon, Dec 3, 2018 at 6:46 PM Leonel Quinteros < >> leonel.quinte...@gmail.com> wrote: >> >>> Hi Ben, >>> >>> I'm pretty sure that the *err* variable is getting shadowed on your >>> Update construct. >>> Instead of doing: >>> >>> if _, err := UpdateBar(tx); err != nil { >>> return err >>> } >>> >>> You should do something like: >>> >>> _, err = UpdateBar(tx) >>> if err != nil { >>> return err >>> } >>> >>> Just like you do with the insert and avoiding the *:=* operand >>> >>> Let me know if that works. >>> >>> >>> Best! >>> Leonel >>> >>> >>> El lunes, 3 de diciembre de 2018, 18:53:30 (UTC-3), Ben Hoyt escribió: >>>> >>>> Hi folks, >>>> >>>> We found some subtle bugs in our db transaction code for handling >>>> commits/rollbacks. Here's the pattern we were using (not real, but shows >>>> the issue): >>>> >>>> func DoTwoThings() error { >>>> tx, err := db.Begin() >>>> if err != nil { >>>> return err >>>> } >>>> // commit or rollback the transaction before we return >>>> defer tx.Close(&err) >>>> >>>> err := InsertFoo(tx) >>>> if err != nil { >>>> return err >>>> } >>>> if _, err := UpdateBar(tx); err != nil { >>>> return err >>>> } >>>> return nil >>>> } >>>> >>>> The problem is there's a subtle but potentially quite bad bug with this >>>> usage pattern -- if the InsertFoo succeeds but UpdateBar fails, the first >>>> "err" variable will be nil, so the deferred tx.Close() will COMMIT the >>>> transaction rather than ROLLBACK, and the database will be in an >>>> inconsistent state. >>>> >>>> The code above is a bit contrived, and you can easily fix it by moving >>>> the "_, err := UpdateBar()" outside of the if so it's referring to the same >>>> "err" variable, but it's very easy to miss and get it wrong. So we decided >>>> it was a bad pattern and started thinking about the best way to fix. >>>> >>>> One idea is a RollbackUnlessCommitted() function which you can defer, >>>> and then you call Commit() once manually (stolen from gocraft/dbr): >>>> >>>> func DoTwoThings() error { >>>> tx, err := db.Begin() >>>> if err != nil { >>>> return err >>>> } >>>> defer tx.RollbackUnlessCommitted() >>>> >>>> err := InsertFoo(tx) >>>> if err != nil { >>>> return err >>>> } >>>> if _, err := UpdateBar(tx); err != nil { >>>> return err >>>> } >>>> tx.Commit() >>>> return nil >>>> } >>>> >>>> Another idea is to create a "Transact" function which takes an >>>> anonymous function and does all the transaction handling: >>>> >>>> func (db *DatabaseImpl) Transact(txFunc func() error) (err error) { >>>> tx, err := db.Begin() >>>> if err != nil { >>>> return >>>> } >>>> defer func() { >>>> if p := recover(); p != nil { >>>> tx.Rollback() >>>> panic(p) // re-throw panic after Rollback >>>> } else if err != nil { >>>> tx.Rollback() // err is non-nil; don't change it >>>> } else { >>>> err = tx.Commit() // err is nil; if Commit returns error >>>> update err >>>> } >>>> }() >>>> err = txFunc(tx) >>>> return err >>>> } >>>> >>>> And then the DoTwoThings function becomes: >>>> >>>> func DoTwoThings() error { >>>> return db.Transact(func() error) { >>>> err := InsertFoo(tx) >>>> if err != nil { >>>> return err >>>> } >>>> if _, err := UpdateBar(tx); err != nil { >>>> return err >>>> } >>>> }) >>>> } >>>> >>>> I think the second is probably safer and nicer, but it's slightly >>>> awkward in that it requires an extra level of indentation. Still, awkward >>>> is better than buggy. >>>> >>>> Does anyone else have a better pattern for this kind of thing, or >>>> feedback on the above? >>>> >>>> -Ben >>>> >>>> -- >>> You received this message because you are subscribed to a topic in the >>> Google Groups "golang-nuts" group. >>> To unsubscribe from this topic, visit >>> https://groups.google.com/d/topic/golang-nuts/ge49ywYnjio/unsubscribe. >>> To unsubscribe from this group and all its topics, send an email to >>> golang-nuts+unsubscr...@googlegroups.com. >>> For more options, visit https://groups.google.com/d/optout. >>> >> -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.