// Copyright 2014 Google Inc. All Rights Reserved. // // 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 datastore import ( "errors" "net/http" "github.com/golang/protobuf/proto" "golang.org/x/net/context" pb "google.golang.org/cloud/internal/datastore" "google.golang.org/cloud/internal/transport" ) // ErrConcurrentTransaction is returned when a transaction is rolled back due // to a conflict with a concurrent transaction. var ErrConcurrentTransaction = errors.New("datastore: concurrent transaction") var errExpiredTransaction = errors.New("datastore: transaction expired") // A TransactionOption configures the Transaction returned by NewTransaction. type TransactionOption interface { apply(*pb.BeginTransactionRequest) } type isolation struct { level pb.BeginTransactionRequest_IsolationLevel } func (i isolation) apply(req *pb.BeginTransactionRequest) { req.IsolationLevel = i.level.Enum() } var ( // Snapshot causes the transaction to enforce a snapshot isolation level. Snapshot TransactionOption = isolation{pb.BeginTransactionRequest_SNAPSHOT} // Serializable causes the transaction to enforce a serializable isolation level. Serializable TransactionOption = isolation{pb.BeginTransactionRequest_SERIALIZABLE} ) // Transaction represents a set of datastore operations to be committed atomically. // // Operations are enqueued by calling the Put and Delete methods on Transaction // (or their Multi-equivalents). These operations are only committed when the // Commit method is invoked. To ensure consistency, reads must be performed by // using Transaction's Get method or by using the Transaction method when // building a query. // // A Transaction must be committed or rolled back exactly once. type Transaction struct { id []byte client *Client ctx context.Context mutation *pb.Mutation // The mutations to apply. pending []*PendingKey // Incomplete keys pending transaction completion. } // NewTransaction starts a new transaction. func (c *Client) NewTransaction(ctx context.Context, opts ...TransactionOption) (*Transaction, error) { req, resp := &pb.BeginTransactionRequest{}, &pb.BeginTransactionResponse{} for _, o := range opts { o.apply(req) } if err := c.call(ctx, "beginTransaction", req, resp); err != nil { return nil, err } return &Transaction{ id: resp.Transaction, ctx: ctx, client: c, mutation: &pb.Mutation{}, }, nil } // Commit applies the enqueued operations atomically. func (t *Transaction) Commit() (*Commit, error) { if t.id == nil { return nil, errExpiredTransaction } req := &pb.CommitRequest{ Transaction: t.id, Mutation: t.mutation, Mode: pb.CommitRequest_TRANSACTIONAL.Enum(), } t.id = nil resp := &pb.CommitResponse{} if err := t.client.call(t.ctx, "commit", req, resp); err != nil { if e, ok := err.(*transport.ErrHTTP); ok && e.StatusCode == http.StatusConflict { // TODO(jbd): Make sure that we explicitly handle the case where response // has an HTTP 409 and the error message indicates that it's an concurrent // transaction error. return nil, ErrConcurrentTransaction } return nil, err } // Copy any newly minted keys into the returned keys. if len(t.pending) != len(resp.MutationResult.InsertAutoIdKey) { return nil, errors.New("datastore: internal error: server returned the wrong number of keys") } commit := &Commit{} for i, p := range t.pending { p.key = protoToKey(resp.MutationResult.InsertAutoIdKey[i]) p.commit = commit } return commit, nil } // Rollback abandons a pending transaction. func (t *Transaction) Rollback() error { if t.id == nil { return errExpiredTransaction } id := t.id t.id = nil return t.client.call(t.ctx, "rollback", &pb.RollbackRequest{Transaction: id}, &pb.RollbackResponse{}) } // Get is the transaction-specific version of the package function Get. // All reads performed during the transaction will come from a single consistent // snapshot. Furthermore, if the transaction is set to a serializable isolation // level, another transaction cannot concurrently modify the data that is read // or modified by this transaction. func (t *Transaction) Get(key *Key, dst interface{}) error { err := t.client.get(t.ctx, []*Key{key}, []interface{}{dst}, &pb.ReadOptions{Transaction: t.id}) if me, ok := err.(MultiError); ok { return me[0] } return err } // GetMulti is a batch version of Get. func (t *Transaction) GetMulti(keys []*Key, dst interface{}) error { if t.id == nil { return errExpiredTransaction } return t.client.get(t.ctx, keys, dst, &pb.ReadOptions{Transaction: t.id}) } // Put is the transaction-specific version of the package function Put. // // Put returns a PendingKey which can be resolved into a Key using the // return value from a successful Commit. If key is an incomplete key, the // returned pending key will resolve to a unique key generated by the // datastore. func (t *Transaction) Put(key *Key, src interface{}) (*PendingKey, error) { h, err := t.PutMulti([]*Key{key}, []interface{}{src}) if err != nil { if me, ok := err.(MultiError); ok { return nil, me[0] } return nil, err } return h[0], nil } // PutMulti is a batch version of Put. One PendingKey is returned for each // element of src in the same order. func (t *Transaction) PutMulti(keys []*Key, src interface{}) ([]*PendingKey, error) { if t.id == nil { return nil, errExpiredTransaction } mutation, err := putMutation(keys, src) if err != nil { return nil, err } proto.Merge(t.mutation, mutation) // Prepare the returned handles, pre-populating where possible. ret := make([]*PendingKey, len(keys)) for i, key := range keys { h := &PendingKey{} if key.Incomplete() { // This key will be in the final commit result. t.pending = append(t.pending, h) } else { h.key = key } ret[i] = h } return ret, nil } // Delete is the transaction-specific version of the package function Delete. // Delete enqueues the deletion of the entity for the given key, to be // committed atomically upon calling Commit. func (t *Transaction) Delete(key *Key) error { err := t.DeleteMulti([]*Key{key}) if me, ok := err.(MultiError); ok { return me[0] } return err } // DeleteMulti is a batch version of Delete. func (t *Transaction) DeleteMulti(keys []*Key) error { if t.id == nil { return errExpiredTransaction } mutation, err := deleteMutation(keys) if err != nil { return err } proto.Merge(t.mutation, mutation) return nil } // Commit represents the result of a committed transaction. type Commit struct{} // Key resolves a pending key handle into a final key. func (c *Commit) Key(p *PendingKey) *Key { if c != p.commit { panic("PendingKey was not created by corresponding transaction") } return p.key } // PendingKey represents the key for newly-inserted entity. It can be // resolved into a Key by calling the Key method of Commit. type PendingKey struct { key *Key commit *Commit }