// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package firestore import ( "context" "errors" gax "github.com/googleapis/gax-go/v2" pb "google.golang.org/genproto/googleapis/firestore/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Transaction represents a Firestore transaction. type Transaction struct { c *Client ctx context.Context id []byte writes []*pb.Write maxAttempts int readOnly bool readAfterWrite bool } // A TransactionOption is an option passed to Client.Transaction. type TransactionOption interface { config(t *Transaction) } // MaxAttempts is a TransactionOption that configures the maximum number of times to // try a transaction. In defaults to DefaultTransactionMaxAttempts. func MaxAttempts(n int) maxAttempts { return maxAttempts(n) } type maxAttempts int func (m maxAttempts) config(t *Transaction) { t.maxAttempts = int(m) } // DefaultTransactionMaxAttempts is the default number of times to attempt a transaction. const DefaultTransactionMaxAttempts = 5 // ReadOnly is a TransactionOption that makes the transaction read-only. Read-only // transactions cannot issue write operations, but are more efficient. var ReadOnly = ro{} type ro struct{} func (ro) config(t *Transaction) { t.readOnly = true } var ( // Defined here for testing. errReadAfterWrite = errors.New("firestore: read after write in transaction") errWriteReadOnly = errors.New("firestore: write in read-only transaction") errNestedTransaction = errors.New("firestore: nested transaction") ) type transactionInProgressKey struct{} // RunTransaction runs f in a transaction. f should use the transaction it is given // for all Firestore operations. For any operation requiring a context, f should use // the context it is passed, not the first argument to RunTransaction. // // f must not call Commit or Rollback on the provided Transaction. // // If f returns nil, RunTransaction commits the transaction. If the commit fails due // to a conflicting transaction, RunTransaction retries f. It gives up and returns an // error after a number of attempts that can be configured with the MaxAttempts // option. If the commit succeeds, RunTransaction returns a nil error. // // If f returns non-nil, then the transaction will be rolled back and // this method will return the same error. The function f is not retried. // // Note that when f returns, the transaction is not committed. Calling code // must not assume that any of f's changes have been committed until // RunTransaction returns nil. // // Since f may be called more than once, f should usually be idempotent – that is, it // should have the same result when called multiple times. func (c *Client) RunTransaction(ctx context.Context, f func(context.Context, *Transaction) error, opts ...TransactionOption) error { if ctx.Value(transactionInProgressKey{}) != nil { return errNestedTransaction } db := c.path() t := &Transaction{ c: c, ctx: withResourceHeader(ctx, db), maxAttempts: DefaultTransactionMaxAttempts, } for _, opt := range opts { opt.config(t) } var txOpts *pb.TransactionOptions if t.readOnly { txOpts = &pb.TransactionOptions{ Mode: &pb.TransactionOptions_ReadOnly_{&pb.TransactionOptions_ReadOnly{}}, } } var backoff gax.Backoff // TODO(jba): use other than the standard backoff parameters? // TODO(jba): get backoff time from gRPC trailer metadata? See // extractRetryDelay in https://code.googlesource.com/gocloud/+/master/spanner/retry.go. var err error for i := 0; i < t.maxAttempts; i++ { var res *pb.BeginTransactionResponse res, err = t.c.c.BeginTransaction(t.ctx, &pb.BeginTransactionRequest{ Database: db, Options: txOpts, }) if err != nil { return err } t.id = res.Transaction err = f(context.WithValue(ctx, transactionInProgressKey{}, 1), t) // Read after write can only be checked client-side, so we make sure to check // even if the user does not. if err == nil && t.readAfterWrite { err = errReadAfterWrite } if err != nil { t.rollback() // Prefer f's returned error to rollback error. return err } _, err = t.c.c.Commit(t.ctx, &pb.CommitRequest{ Database: t.c.path(), Writes: t.writes, Transaction: t.id, }) // If a read-write transaction returns Aborted, retry. // On success or other failures, return here. if t.readOnly || grpc.Code(err) != codes.Aborted { // According to the Firestore team, we should not roll back here // if err != nil. But spanner does. // See https://code.googlesource.com/gocloud/+/master/spanner/transaction.go#740. return err } if txOpts == nil { // txOpts can only be nil if is the first retry of a read-write transaction. // (It is only set here and in the body of "if t.readOnly" above.) // Mention the transaction ID in BeginTransaction so the service // knows it is a retry. txOpts = &pb.TransactionOptions{ Mode: &pb.TransactionOptions_ReadWrite_{ &pb.TransactionOptions_ReadWrite{RetryTransaction: t.id}, }, } } // Use exponential backoff to avoid contention with other running // transactions. if cerr := sleep(ctx, backoff.Pause()); cerr != nil { err = cerr break } // Reset state for the next attempt. t.writes = nil } // If we run out of retries, return the last error we saw (which should // be the Aborted from Commit, or a context error). if err != nil { t.rollback() } return err } func (t *Transaction) rollback() { _ = t.c.c.Rollback(t.ctx, &pb.RollbackRequest{ Database: t.c.path(), Transaction: t.id, }) // Ignore the rollback error. // TODO(jba): Log it? // Note: Rollback is idempotent so it will be retried by the gapic layer. } // Get gets the document in the context of the transaction. The transaction holds a // pessimistic lock on the returned document. func (t *Transaction) Get(dr *DocumentRef) (*DocumentSnapshot, error) { docsnaps, err := t.GetAll([]*DocumentRef{dr}) if err != nil { return nil, err } ds := docsnaps[0] if !ds.Exists() { return ds, status.Errorf(codes.NotFound, "%q not found", dr.Path) } return ds, nil } // GetAll retrieves multiple documents with a single call. The DocumentSnapshots are // returned in the order of the given DocumentRefs. If a document is not present, the // corresponding DocumentSnapshot's Exists method will return false. The transaction // holds a pessimistic lock on all of the returned documents. func (t *Transaction) GetAll(drs []*DocumentRef) ([]*DocumentSnapshot, error) { if len(t.writes) > 0 { t.readAfterWrite = true return nil, errReadAfterWrite } return t.c.getAll(t.ctx, drs, t.id) } // A Queryer is a Query or a CollectionRef. CollectionRefs act as queries whose // results are all the documents in the collection. type Queryer interface { query() *Query } // Documents returns a DocumentIterator based on given Query or CollectionRef. The // results will be in the context of the transaction. func (t *Transaction) Documents(q Queryer) *DocumentIterator { if len(t.writes) > 0 { t.readAfterWrite = true return &DocumentIterator{err: errReadAfterWrite} } return &DocumentIterator{ iter: newQueryDocumentIterator(t.ctx, q.query(), t.id), } } // DocumentRefs returns references to all the documents in the collection, including // missing documents. A missing document is a document that does not exist but has // sub-documents. func (t *Transaction) DocumentRefs(cr *CollectionRef) *DocumentRefIterator { if len(t.writes) > 0 { t.readAfterWrite = true return &DocumentRefIterator{err: errReadAfterWrite} } return newDocumentRefIterator(t.ctx, cr, t.id) } // Create adds a Create operation to the Transaction. // See DocumentRef.Create for details. func (t *Transaction) Create(dr *DocumentRef, data interface{}) error { return t.addWrites(dr.newCreateWrites(data)) } // Set adds a Set operation to the Transaction. // See DocumentRef.Set for details. func (t *Transaction) Set(dr *DocumentRef, data interface{}, opts ...SetOption) error { return t.addWrites(dr.newSetWrites(data, opts)) } // Delete adds a Delete operation to the Transaction. // See DocumentRef.Delete for details. func (t *Transaction) Delete(dr *DocumentRef, opts ...Precondition) error { return t.addWrites(dr.newDeleteWrites(opts)) } // Update adds a new Update operation to the Transaction. // See DocumentRef.Update for details. func (t *Transaction) Update(dr *DocumentRef, data []Update, opts ...Precondition) error { return t.addWrites(dr.newUpdatePathWrites(data, opts)) } func (t *Transaction) addWrites(ws []*pb.Write, err error) error { if t.readOnly { return errWriteReadOnly } if err != nil { return err } t.writes = append(t.writes, ws...) return nil }