
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>
163 lines
5.2 KiB
Go
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()
|
|
}
|