containerd/snapshots/storage/metastore.go
Danny Canter 3b71cfd407 metastore: Add WithTransaction convenience method
Most snapshotters end up manually handling the rollback logic, either
by calling `t.Rollback()` in every failure path, setting up a custom
defer func to log on certain errors, or just deferring `t.Rollback()`
even for `snapshotter.Commit()` which *will* cause `t.Rollback()` to return
an error afaict, but it's just never checked and luckily bolt handles this
alright...

The devmapper snapshotter has a solution to this which is to have a
method that starts either a read-only or writable transaction inside
the method, and you pass in a callback to do your bidding and any
failures are rolled back, and if it's writable will handle the commit
for you. This seems like the right model to me, it removes the burden
from the snapshot author to remember to either defer/call rollback
in every method for every failure case.

This change exposes the convenience method from devmapper to the
snapshots/storage package as a method off of `storage.MetaStore` and moves
over the devmapper snapshotter to use this.

Signed-off-by: Danny Canter <danny@dcantah.dev>
2022-12-07 23:49:09 -08:00

163 lines
5.2 KiB
Go

/*
Copyright The containerd Authors.
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 storage provides a metadata storage implementation for snapshot
// drivers. Drive implementations are responsible for starting and managing
// transactions using the defined context creator. This storage package uses
// BoltDB for storing metadata. Access to the raw boltdb transaction is not
// provided, but the stored object is provided by the proto subpackage.
package storage
import (
"context"
"fmt"
"sync"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/snapshots"
"github.com/hashicorp/go-multierror"
bolt "go.etcd.io/bbolt"
)
// Transactor is used to finalize an active transaction.
type Transactor interface {
// Commit commits any changes made during the transaction. On error a
// caller is expected to clean up any resources which would have relied
// on data mutated as part of this transaction. Only writable
// transactions can commit, non-writable must call Rollback.
Commit() error
// Rollback rolls back any changes made during the transaction. This
// must be called on all non-writable transactions and aborted writable
// transaction.
Rollback() error
}
// Snapshot hold the metadata for an active or view snapshot transaction. The
// ParentIDs hold the snapshot identifiers for the committed snapshots this
// active or view is based on. The ParentIDs are ordered from the lowest base
// to highest, meaning they should be applied in order from the first index to
// the last index. The last index should always be considered the active
// snapshots immediate parent.
type Snapshot struct {
Kind snapshots.Kind
ID string
ParentIDs []string
}
// MetaStore is used to store metadata related to a snapshot driver. The
// MetaStore is intended to store metadata related to name, state and
// parentage. Using the MetaStore is not required to implement a snapshot
// driver but can be used to handle the persistence and transactional
// complexities of a driver implementation.
type MetaStore struct {
dbfile string
dbL sync.Mutex
db *bolt.DB
}
// NewMetaStore returns a snapshot MetaStore for storage of metadata related to
// a snapshot driver backed by a bolt file database. This implementation is
// strongly consistent and does all metadata changes in a transaction to prevent
// against process crashes causing inconsistent metadata state.
func NewMetaStore(dbfile string) (*MetaStore, error) {
return &MetaStore{
dbfile: dbfile,
}, nil
}
type transactionKey struct{}
// TransactionContext creates a new transaction context. The writable value
// should be set to true for transactions which are expected to mutate data.
func (ms *MetaStore) TransactionContext(ctx context.Context, writable bool) (context.Context, Transactor, error) {
ms.dbL.Lock()
if ms.db == nil {
db, err := bolt.Open(ms.dbfile, 0600, nil)
if err != nil {
ms.dbL.Unlock()
return ctx, nil, fmt.Errorf("failed to open database file: %w", err)
}
ms.db = db
}
ms.dbL.Unlock()
tx, err := ms.db.Begin(writable)
if err != nil {
return ctx, nil, fmt.Errorf("failed to start transaction: %w", err)
}
ctx = context.WithValue(ctx, transactionKey{}, tx)
return ctx, tx, nil
}
// TransactionCallback represents a callback to be invoked while under a metastore transaction.
type TransactionCallback func(ctx context.Context) error
// WithTransaction is a convenience method to run a function `fn` while holding a meta store transaction.
// If the callback `fn` returns an error or the transaction is not writable, the database transaction will be discarded.
func (ms *MetaStore) WithTransaction(ctx context.Context, writable bool, fn TransactionCallback) error {
ctx, trans, err := ms.TransactionContext(ctx, writable)
if err != nil {
return err
}
var result *multierror.Error
err = fn(ctx)
if err != nil {
result = multierror.Append(result, err)
}
// Always rollback if transaction is not writable
if err != nil || !writable {
if terr := trans.Rollback(); terr != nil {
log.G(ctx).WithError(terr).Error("failed to rollback transaction")
result = multierror.Append(result, fmt.Errorf("rollback failed: %w", terr))
}
} else {
if terr := trans.Commit(); terr != nil {
log.G(ctx).WithError(terr).Error("failed to commit transaction")
result = multierror.Append(result, fmt.Errorf("commit failed: %w", terr))
}
}
if err := result.ErrorOrNil(); err != nil {
log.G(ctx).WithError(err).Debug("snapshotter error")
// Unwrap if just one error
if len(result.Errors) == 1 {
return result.Errors[0]
}
return err
}
return nil
}
// Close closes the metastore and any underlying database connections
func (ms *MetaStore) Close() error {
ms.dbL.Lock()
defer ms.dbL.Unlock()
if ms.db == nil {
return nil
}
return ms.db.Close()
}