1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
12 "git.arvados.org/arvados.git/lib/controller/api"
13 "git.arvados.org/arvados.git/sdk/go/ctxlog"
14 "github.com/jmoiron/sqlx"
16 // sqlx needs lib/pq to talk to PostgreSQL
21 ErrNoTransaction = errors.New("bug: there is no transaction in this context")
22 ErrContextFinished = errors.New("refusing to start a transaction after wrapped function already returned")
25 // WrapCallsInTransactions returns a call wrapper (suitable for
26 // assigning to router.router.WrapCalls) that starts a new transaction
27 // for each API call, and commits only if the call succeeds.
29 // The wrapper calls getdb() to get a database handle before each API
31 func WrapCallsInTransactions(getdb func(context.Context) (*sqlx.DB, error)) func(api.RoutableFunc) api.RoutableFunc {
32 return func(origFunc api.RoutableFunc) api.RoutableFunc {
33 return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
34 ctx, finishtx := New(ctx, getdb)
36 return origFunc(ctx, opts)
41 // NewWithTransaction returns a child context in which the given
42 // transaction will be used by any localdb API call that needs one.
43 // The caller is responsible for calling Commit or Rollback on tx.
44 func NewWithTransaction(ctx context.Context, tx *sqlx.Tx) context.Context {
45 txn := &transaction{tx: tx}
46 txn.setup.Do(func() {})
47 return context.WithValue(ctx, contextKeyTransaction, txn)
50 type contextKeyT string
52 var contextKeyTransaction = contextKeyT("transaction")
54 type transaction struct {
57 getdb func(context.Context) (*sqlx.DB, error)
61 type finishFunc func(*error)
63 // New returns a new child context that can be used with
64 // CurrentTx(). It does not open a database transaction until the
65 // first call to CurrentTx().
67 // The caller must eventually call the returned finishtx() func to
68 // commit or rollback the transaction, if any.
70 // func example(ctx context.Context) (err error) {
71 // ctx, finishtx := New(ctx, dber)
72 // defer finishtx(&err)
74 // tx, err := CurrentTx(ctx)
76 // return fmt.Errorf("example: %s", err)
78 // return tx.ExecContext(...)
81 // If *err is nil, finishtx() commits the transaction and assigns any
82 // resulting error to *err.
84 // If *err is non-nil, finishtx() rolls back the transaction, and
85 // does not modify *err.
86 func New(ctx context.Context, getdb func(context.Context) (*sqlx.DB, error)) (context.Context, finishFunc) {
87 txn := &transaction{getdb: getdb}
88 return context.WithValue(ctx, contextKeyTransaction, txn), func(err *error) {
90 // Using (*sync.Once)Do() prevents a future
91 // call to CurrentTx() from opening a
92 // transaction which would never get committed
93 // or rolled back. If CurrentTx() hasn't been
94 // called before now, future calls will return
96 txn.err = ErrContextFinished
99 // we never [successfully] started a transaction
103 ctxlog.FromContext(ctx).Debug("rollback")
107 *err = txn.tx.Commit()
111 // NewTx starts a new transaction. The caller is responsible for
112 // calling Commit or Rollback. This is suitable for database queries
113 // that are separate from the API transaction (see CurrentTx), e.g.,
114 // ones that will be committed even if the API call fails, or held
115 // open after the API call finishes.
116 func NewTx(ctx context.Context) (*sqlx.Tx, error) {
117 txn, ok := ctx.Value(contextKeyTransaction).(*transaction)
119 return nil, ErrNoTransaction
121 db, err := txn.getdb(ctx)
128 // CurrentTx returns a transaction that will be committed after the
129 // current API call completes, or rolled back if the current API call
131 func CurrentTx(ctx context.Context) (*sqlx.Tx, error) {
132 txn, ok := ctx.Value(contextKeyTransaction).(*transaction)
134 return nil, ErrNoTransaction
136 txn.setup.Do(func() {
137 if db, err := txn.getdb(ctx); err != nil {
140 txn.tx, txn.err = db.Beginx()
143 return txn.tx, txn.err