storing transaction in context

3,054 views
Skip to first unread message

Steven Roth

unread,
Feb 6, 2017, 9:57:25 PM2/6/17
to golang-nuts
I'd like input on whether the following idea is good or bad, and why.

Consider an abstract transaction, modeled as follows:
func Transaction(ctx context.Context, body func(ctx context.Context) error) error
func OnCommit(ctx context.Context, commitHook func(ctx context.Context))
func OnRollback(ctx context.Context, rollbackHook func(ctx context.Context))

The code that wishes to execute a transaction would call Transaction and provide the body of the transaction as a function parameter.  If that function returns an error, the transaction is rolled back, by calling any hooks registered during that transaction by calling OnRollback.  If the body function returns success, the transaction is committed by calling any hooks registered during that transaction by calling OnCommit.

The implication of this model is that the identity of what transaction you're in is stored in the context.  The ctx passed into body has that value added to it, and the OnCommit and OnRollback calls associate the hooks with the transaction they find in the context.

Of course the same thing could be done by passing the transaction explicitly, e.g.
func Transaction(ctx context.Context, body func(ctx context.Context, tx Tx) error) error
func OnCommit(tx Tx, commitHook func(ctx context.Context))
func OnRollback(tx Tx, rollbackHook func(ctx context.Context))

But this would mean that both ctx and tx would have to be passed in parameters to all functions called during the body of the transaction.  My question is whether putting the transaction in the context using context.WithValue is a reasonable and appropriate use of that mechanism.  If not, why not?

The use case for this transaction model is not really relevant to the question, but I'll include it for illustrative purposes.  Imagine a web server implemented in layers, e.g. transport on top of service on top of database access.  One would like to promote the transaction concept out of the database access layer so that multiple calls from the service layer to the database access layer could occur within the same database transaction.  One way to do that would be to wrap the service call in an abstract transaction, and have the database layer honor that with database transactions in an encapsulated fashion.

Many thanks in advance for your reactions and wisdom.
Steve

Dave Cheney

unread,
Feb 6, 2017, 10:50:52 PM2/6/17
to golang-nuts
I'd say store that context in your transaction value, not the other way around.

Chetan Gowda

unread,
Feb 7, 2017, 1:12:57 PM2/7/17
to golang-nuts
@Dave, isn't storing context in some struct considered anti-pattern? From the package doc:
"
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx
"

In this case, what are the drawbacks of storing transaction in the context?

Dave Cheney

unread,
Feb 7, 2017, 1:37:02 PM2/7/17
to golang-nuts
I guess it depends on how long your transaction lasts for; it doesn't sound like it lives for that long. IMO the advice about storing contexts in other objects is more about "don't put this into a long lived object", like your server's main loop or something.
I wrote some general comments about context.Value here, https://dave.cheney.net/2017/01/26/context-is-for-cancelation, Peter Bourgon has also written about using conext.Value https://peter.bourgon.org/blog/2016/07/11/context.html, as has Jack Lindamood, https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39 

Steve Roth

unread,
Feb 7, 2017, 10:33:09 PM2/7/17
to golang-nuts
Fascinating.  I didn't entertain the idea of putting the context in the transaction, for exactly the reason that Chetan cited.  But it does seem like a good answer.  (And yes, the transactions are short.  Long-running transactions are an anti-pattern in their own right.)

Many thanks for the input!
Steve

Manlio Perillo

unread,
Feb 8, 2017, 6:46:01 AM2/8/17
to golang-nuts
Il giorno martedì 7 febbraio 2017 03:57:25 UTC+1, Steve Roth ha scritto:
I'd like input on whether the following idea is good or bad, and why.

Consider an abstract transaction, modeled as follows:
func Transaction(ctx context.Context, body func(ctx context.Context) error) error

Is Transaction a method of your database connection type?
 
func OnCommit(ctx context.Context, commitHook func(ctx context.Context))
func OnRollback(ctx context.Context, rollbackHook func(ctx context.Context))

The code that wishes to execute a transaction would call Transaction and provide the body of the transaction as a function parameter.  If that function returns an error, the transaction is rolled back, by calling any hooks registered during that transaction by calling OnRollback.  If the body function returns success, the transaction is committed by calling any hooks registered during that transaction by calling OnCommit.

The implication of this model is that the identity of what transaction you're in is stored in the context.  The ctx passed into body has that value added to it, and the OnCommit and OnRollback calls associate the hooks with the transaction they find in the context.

Of course the same thing could be done by passing the transaction explicitly, e.g.
func Transaction(ctx context.Context, body func(ctx context.Context, tx Tx) error) error

There is one problem here.

Suppose you have a function that needs to query the database.
This function *should* work both when in auto commit mode or when inside a transaction.
However sql.DB and sql.Tx are concrete types, so you can not pass both of them to the function.
 
> [...]

Manlio

Jakob Borg

unread,
Feb 8, 2017, 7:02:46 AM2/8/17
to golang-nuts
On 7 Feb 2017, at 03:57, Steven Roth <st...@rothskeller.net> wrote:
> My question is whether putting the transaction in the context using context.WithValue is a reasonable and appropriate use of that mechanism. If not, why not?

In my opinion, this is morally equivalent to a

func Something(params map[interface{}]interface{})

which is a horrible API that no-one here would accept. If the function needs a sql.Tx that should be one of the parameters. That the function also takes a Context is beside the point.

//jb

Henrik Johansson

unread,
Feb 8, 2017, 7:14:49 AM2/8/17
to Jakob Borg, golang-nuts
The Context as "a bag of stuff" is attractive since it makes your code easier to refactor (knock, knock) but gives me void* or as Jakob said map[string]interface{} vibes.

I think I think it should be avoided but probably not unconditionally. I guess there can be cases where it makes sense.


--
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...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Sameer Ajmani

unread,
Feb 8, 2017, 7:56:37 AM2/8/17
to Henrik Johansson, Jakob Borg, golang-nuts
Historically we've insisted that the Context be passed explicitly so that it's always visible in the code and accessible to refactoring tools. In reality, tools that assist in context plumbing don't exist yet, and there are examples of putting Context inside some other structs in the standard library, like http.Request. A Transaction pays a similar role to a Request in programs, so I can see an argument for putting the Context inside it. But I prefer the explicit code with both parameters. This makes it clear to the reader that the functions are operating within a Transaction and where to find the Context for each call.
Reply all
Reply to author
Forward
0 new messages