blob: 44c37e7823b16b96d9fd6179a1ed5d22e4e99200 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Implements a map-based store substitute that implements the legacy store API.
package fs
import (
"encoding/gob"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"sort"
"strings"
"sync"
"time"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/services/application"
"v.io/v23/verror"
"v.io/v23/vom"
"v.io/x/lib/set"
"v.io/x/ref/services/profile"
)
// TODO(rjkroege@google.com) Switch Memstore to the mid-August 2014
// style store API.
const pkgPath = "v.io/x/ref/services/internal/fs"
// Errors
var (
ErrNoRecursiveCreateTransaction = verror.Register(pkgPath+".ErrNoRecursiveCreateTransaction", verror.NoRetry, "{1:}{2:} recursive CreateTransaction() not permitted{:_}")
ErrDoubleCommit = verror.Register(pkgPath+".ErrDoubleCommit", verror.NoRetry, "{1:}{2:} illegal attempt to commit previously committed or abandonned transaction{:_}")
ErrAbortWithoutTransaction = verror.Register(pkgPath+".ErrAbortWithoutTransaction", verror.NoRetry, "{1:}{2:} illegal attempt to abort non-existent transaction{:_}")
ErrWithoutTransaction = verror.Register(pkgPath+".ErrRemoveWithoutTransaction", verror.NoRetry, "{1:}{2:} call without a transaction{:_}")
ErrNotInMemStore = verror.Register(pkgPath+".ErrNotInMemStore", verror.NoRetry, "{1:}{2:} not in Memstore{:_}")
ErrUnsupportedType = verror.Register(pkgPath+".ErrUnsupportedType", verror.NoRetry, "{1:}{2:} attempted Put to Memstore of unsupported type{:_}")
ErrChildrenWithoutLock = verror.Register(pkgPath+".ErrChildrenWithoutLock", verror.NoRetry, "{1:}{2:} Children() without a lock{:_}")
errTempFileFailed = verror.Register(pkgPath+".errTempFileFailed", verror.NoRetry, "{1:}{2:} TempFile({3}, {4}) failed{:_}")
errCantCreate = verror.Register(pkgPath+".errCantCreate", verror.NoRetry, "{1:}{2:} File ({3}) could not be created ({4}){:_}")
errCantOpen = verror.Register(pkgPath+".errCantOpen", verror.NoRetry, "{1:}{2:} File ({3}) could not be opened ({4}){:_}")
errDecodeFailedBadData = verror.Register(pkgPath+".errDecodeFailedBadData", verror.NoRetry, "{1:}{2:} Decode() failed: data format mismatch or backing file truncated{:_}")
errCreateFailed = verror.Register(pkgPath+".errCreateFailed", verror.NoRetry, "{1:}{2:} Create({3}) failed{:_}")
errEncodeFailed = verror.Register(pkgPath+".errEncodeFailed", verror.NoRetry, "{1:}{2:} Encode() failed{:_}")
errFileSystemError = verror.Register(pkgPath+".errFileSystemError", verror.NoRetry, "{1:}{2:} File system operation failed{:_}")
errFormatUpgradeError = verror.Register(pkgPath+".errFormatUpgradeError", verror.NoRetry, "{1:}{2:} File format upgrading failed{:_}")
)
// Memstore contains the state of the memstore. It supports a single
// transaction at a time. The current state of a Memstore under a
// transactional name binding is the contents of puts then the contents
// of (data - removes). puts and removes will be empty at the beginning
// of a transaction and after an Unlock operation.
type Memstore struct {
sync.Mutex
persistedFile string
haveTransactionNameBinding bool
locked bool
data map[string]interface{}
puts map[string]interface{}
removes map[string]struct{}
}
const (
startingMemstoreSize = 10
transactionNamePrefix = "memstore-transaction"
// A memstore is a serialized Go map. A GOB-encoded map using Go 1.4
// cannot begin with this byte. Consequently, simplestore writes this
// magic byte to the start of a vom file. Its absence at the
// beginning of the file indicates that the memstore file is using
// the GOB legacy encoding.
vomGobMagicByte = 0xF0
)
var keyExists = struct{}{}
// TODO(rjkroege): Simplestore used GOB for its persistence
// layer. However, now, it uses VOM to store persisted values and
// automatically converts GOB format files to VOM-compatible on load.
// At some point in the future, it may be possible to simplify the
// implementation by removing support for loading files in the
// now legacy GOB format.
type applicationEnvelope struct {
Title string
Args []string
Binary application.SignedFile
Publisher security.WireBlessings
Env []string
Packages application.Packages
Restarts int32
RestartTimeWindow time.Duration
}
// This function is needed only to support existing serialized data and
// can be removed in a future release.
func translateFromGobEncodeable(in interface{}) (interface{}, error) {
env, ok := in.(applicationEnvelope)
if !ok {
return in, nil
}
// Have to roundtrip through vom to convert from WireBlessings to Blessings.
// This may seem silly, but this whole translation business is silly too :)
// and will go away once we switch this package to using 'vom' instead of 'gob'.
// So for now, live with the funkiness.
bytes, err := vom.Encode(env.Publisher)
if err != nil {
return nil, err
}
var publisher security.Blessings
if err := vom.Decode(bytes, &publisher); err != nil {
return nil, err
}
return application.Envelope{
Title: env.Title,
Args: env.Args,
Binary: env.Binary,
Publisher: publisher,
Env: env.Env,
Packages: env.Packages,
Restarts: env.Restarts,
RestartTimeWindow: env.RestartTimeWindow,
}, nil
}
// The implementation of set requires gob instead of json.
func init() {
gob.Register(profile.Specification{})
gob.Register(applicationEnvelope{})
gob.Register(access.Permissions{})
// Ensure that no fields have been added to application.Envelope,
// because if so, then applicationEnvelope defined in this package
// needs to change
if n := reflect.TypeOf(application.Envelope{}).NumField(); n != 8 {
panic(fmt.Sprintf("It appears that fields have been added to or removed from application.Envelope before the hack in this file around gob-encodeability was removed. Please also update applicationEnvelope, translateToGobEncodeable and translateToGobDecodeable in this file"))
}
}
// isVOM returns true if the file is a VOM-format file.
func isVOM(file io.ReadSeeker) (bool, error) {
oneByte := make([]byte, 1)
c, err := file.Read(oneByte)
for c == 0 && err == nil {
c, err = file.Read(oneByte)
}
if c > 0 {
if oneByte[0] == vomGobMagicByte {
return true, nil
} else {
if _, err := file.Seek(0, 0); err != nil {
return false, verror.New(errFileSystemError, nil, err)
}
return false, nil
}
}
return false, err
}
func nonEmptyExists(fileName string) (bool, error) {
fi, serr := os.Stat(fileName)
if os.IsNotExist(serr) {
return false, nil
} else if serr != nil {
return false, serr
}
if fi.Size() > 0 {
return true, nil
}
return false, nil
}
func convertFileToVomIfNecessary(fileName string) error {
// Open the legacy file.
file, err := os.Open(fileName)
if err != nil {
return verror.New(errCantOpen, nil, fileName, err)
}
defer file.Close()
// VOM files don't need conversion.
if is, err := isVOM(file); is || err != nil {
return err
}
// Decode the legacy GOB file
decoder := gob.NewDecoder(file)
data := make(map[string]interface{}, startingMemstoreSize)
if err := decoder.Decode(&data); err != nil {
return verror.New(errDecodeFailedBadData, nil, err)
}
// Update GOB file to VOM format in memory.
for k, v := range data {
tv, err := translateFromGobEncodeable(v)
if err != nil {
return verror.New(errFormatUpgradeError, nil, err)
}
data[k] = tv
}
ms := &Memstore{
data: data,
persistedFile: fileName,
}
if err := ms.persist(); err != nil {
return err
}
return nil
}
// NewMemstore persists the Memstore to os.TempDir() if no file is
// configured.
func NewMemstore(configuredPersistentFile string) (*Memstore, error) {
data := make(map[string]interface{}, startingMemstoreSize)
if configuredPersistentFile == "" {
f, err := ioutil.TempFile(os.TempDir(), "memstore-vom")
if err != nil {
return nil, verror.New(errTempFileFailed, nil, os.TempDir(), "memstore-vom", err)
}
f.Close()
configuredPersistentFile = f.Name()
}
// Empty or non-existent files.
nee, err := nonEmptyExists(configuredPersistentFile)
if err != nil {
return nil, verror.New(errCantCreate, nil, configuredPersistentFile, err)
}
if !nee {
// Ensure that we can create a file.
file, cerr := os.Create(configuredPersistentFile)
if cerr != nil {
return nil, verror.New(errCantCreate, nil, configuredPersistentFile, err, cerr)
}
file.Close()
return &Memstore{
data: data,
persistedFile: configuredPersistentFile,
}, nil
}
// Convert a non-empty GOB file into VOM format.
if err := convertFileToVomIfNecessary(configuredPersistentFile); err != nil {
return nil, err
}
file, err := os.Open(configuredPersistentFile)
if err != nil {
return nil, verror.New(errCantOpen, nil, configuredPersistentFile, err)
}
// Skip past the magic byte that identifies this as a VOM format file.
if _, err := file.Seek(1, 0); err != nil {
return nil, verror.New(errFileSystemError, nil, err)
}
decoder := vom.NewDecoder(file)
if err := decoder.Decode(&data); err != nil {
return nil, verror.New(errDecodeFailedBadData, nil, err)
}
return &Memstore{
data: data,
persistedFile: configuredPersistentFile,
}, nil
}
type MemstoreObject interface {
Remove(_ interface{}) error
Exists(_ interface{}) (bool, error)
}
type boundObject struct {
path string
ms *Memstore
Value interface{}
}
// BindObject sets the path string for subsequent operations.
func (ms *Memstore) BindObject(path string) *boundObject {
pathParts := strings.SplitN(path, "/", 2)
if pathParts[0] == transactionNamePrefix {
ms.haveTransactionNameBinding = true
} else {
ms.haveTransactionNameBinding = false
}
return &boundObject{path: pathParts[1], ms: ms}
}
func (ms *Memstore) removeChildren(path string) bool {
deleted := false
for k, _ := range ms.data {
if strings.HasPrefix(k, path) {
deleted = true
ms.removes[k] = keyExists
}
}
for k, _ := range ms.puts {
if strings.HasPrefix(k, path) {
deleted = true
delete(ms.puts, k)
}
}
return deleted
}
type Transaction interface {
CreateTransaction(_ interface{}) (string, error)
Commit(_ interface{}) error
}
// BindTransactionRoot on a Memstore always operates over the
// entire Memstore. As a result, the root parameter is ignored.
func (ms *Memstore) BindTransactionRoot(_ string) Transaction {
return ms
}
// BindTransaction on a Memstore can only use the single Memstore
// transaction.
func (ms *Memstore) BindTransaction(_ string) Transaction {
return ms
}
func (ms *Memstore) newTransactionState() {
ms.puts = make(map[string]interface{}, startingMemstoreSize)
ms.removes = make(map[string]struct{}, startingMemstoreSize)
}
func (ms *Memstore) clearTransactionState() {
ms.puts = nil
ms.removes = nil
}
// Unlock abandons an in-progress transaction before releasing the lock.
func (ms *Memstore) Unlock() {
ms.locked = false
ms.clearTransactionState()
ms.Mutex.Unlock()
}
// Lock acquires a lock and caches the state of the lock.
func (ms *Memstore) Lock() {
ms.Mutex.Lock()
ms.clearTransactionState()
ms.locked = true
}
// CreateTransaction requires the caller to acquire a lock on the Memstore.
func (ms *Memstore) CreateTransaction(_ interface{}) (string, error) {
if ms.puts != nil || ms.removes != nil {
return "", verror.New(ErrNoRecursiveCreateTransaction, nil)
}
ms.newTransactionState()
return transactionNamePrefix, nil
}
// Commit updates the store and persists the result.
func (ms *Memstore) Commit(_ interface{}) error {
if !ms.locked || ms.puts == nil || ms.removes == nil {
return verror.New(ErrDoubleCommit, nil)
}
for k, v := range ms.puts {
ms.data[k] = v
}
for k, _ := range ms.removes {
delete(ms.data, k)
}
return ms.persist()
}
func (ms *Memstore) Abort(_ interface{}) error {
if !ms.locked {
return verror.New(ErrAbortWithoutTransaction, nil)
}
return nil
}
func (o *boundObject) Remove(_ interface{}) error {
if !o.ms.locked {
return verror.New(ErrWithoutTransaction, nil, "Remove()")
}
if _, pendingRemoval := o.ms.removes[o.path]; pendingRemoval {
return verror.New(ErrNotInMemStore, nil, o.path)
}
_, found := o.ms.data[o.path]
if !found && !o.ms.removeChildren(o.path) {
return verror.New(ErrNotInMemStore, nil, o.path)
}
delete(o.ms.puts, o.path)
o.ms.removes[o.path] = keyExists
return nil
}
// transactionExists implements Exists() for bound names that have the
// transaction prefix.
func (o *boundObject) transactionExists() bool {
// Determine if the bound name point to a real object.
_, inBase := o.ms.data[o.path]
_, inPuts := o.ms.puts[o.path]
_, inRemoves := o.ms.removes[o.path]
// not yet committed.
if inPuts || (inBase && !inRemoves) {
return true
}
// The bound names might be a prefix of the path for a real object. For
// example, BindObject("/test/a"). Put(o) creates a real object o at path
/// test/a so the code above will cause BindObject("/test/a").Exists() to
// return true. Testing this is however not sufficient because
// BindObject(any prefix of on object path).Exists() needs to also be
// true. For example, here BindObject("/test").Exists() is true.
//
// Consequently, transactionExists scans all object names in the Memstore
// to determine if any of their names have the bound name as a prefix.
// Puts take precedence over removes so we scan it first.
for k, _ := range o.ms.puts {
if strings.HasPrefix(k, o.path) {
return true
}
}
// Then we scan data for matches and verify that at least one of the
// object names with the bound prefix have not been removed.
for k, _ := range o.ms.data {
if _, inRemoves := o.ms.removes[k]; strings.HasPrefix(k, o.path) && !inRemoves {
return true
}
}
return false
}
func (o *boundObject) Exists(_ interface{}) (bool, error) {
if o.ms.haveTransactionNameBinding {
return o.transactionExists(), nil
} else {
_, inBase := o.ms.data[o.path]
if inBase {
return true, nil
}
for k, _ := range o.ms.data {
if strings.HasPrefix(k, o.path) {
return true, nil
}
}
}
return false, nil
}
// transactionBoundGet implements Get while the bound name has the
// transaction prefix.
func (o *boundObject) transactionBoundGet() (*boundObject, error) {
bv, inBase := o.ms.data[o.path]
_, inRemoves := o.ms.removes[o.path]
pv, inPuts := o.ms.puts[o.path]
found := inPuts || (inBase && !inRemoves)
if !found {
return nil, verror.New(ErrNotInMemStore, nil, o.path)
}
if inPuts {
o.Value = pv
} else {
o.Value = bv
}
var err error
if o.Value, err = translateFromGobEncodeable(o.Value); err != nil {
return nil, err
}
return o, nil
}
func (o *boundObject) bareGet() (*boundObject, error) {
bv, inBase := o.ms.data[o.path]
if !inBase {
return nil, verror.New(ErrNotInMemStore, nil, o.path)
}
o.Value = bv
return o, nil
}
func (o *boundObject) Get(_ interface{}) (*boundObject, error) {
if o.ms.haveTransactionNameBinding {
return o.transactionBoundGet()
} else {
return o.bareGet()
}
}
func (o *boundObject) Put(_ interface{}, envelope interface{}) (*boundObject, error) {
if !o.ms.locked {
return nil, verror.New(ErrWithoutTransaction, nil, "Put()")
}
switch v := envelope.(type) {
case application.Envelope, profile.Specification, access.Permissions:
o.ms.puts[o.path] = v
delete(o.ms.removes, o.path)
o.Value = o.path
return o, nil
default:
return o, verror.New(ErrUnsupportedType, nil)
}
}
func (o *boundObject) Children() ([]string, error) {
if !o.ms.locked {
return nil, verror.New(ErrChildrenWithoutLock, nil)
}
found := false
childrenSet := make(map[string]struct{})
for k, _ := range o.ms.data {
if strings.HasPrefix(k, o.path) || o.path == "" {
name := strings.TrimPrefix(k, o.path)
// Found the object itself.
if len(name) == 0 {
found = true
continue
}
// This was only a prefix match, not what we're looking for.
if name[0] != '/' && o.path != "" {
continue
}
found = true
name = strings.TrimLeft(name, "/")
if idx := strings.Index(name, "/"); idx != -1 {
name = name[:idx]
}
childrenSet[name] = keyExists
}
}
if !found {
return nil, verror.New(verror.ErrNoExist, nil, o.path)
}
children := set.String.ToSlice(childrenSet)
sort.Strings(children)
return children, nil
}
// persist() writes the state of the Memstore to persistent storage.
func (ms *Memstore) persist() error {
file, err := ioutil.TempFile(os.TempDir(), "memstore-persisting")
if err != nil {
return verror.New(errTempFileFailed, nil, os.TempDir(), "memstore-persisting", err)
}
defer file.Close()
defer os.Remove(file.Name())
// Mark this VOM file with the VOM file format magic byte.
if _, err := file.Write([]byte{byte(vomGobMagicByte)}); err != nil {
return err
}
enc := vom.NewEncoder(file)
if err := enc.Encode(ms.data); err != nil {
return verror.New(errEncodeFailed, nil, err)
}
ms.clearTransactionState()
if err := os.Rename(file.Name(), ms.persistedFile); err != nil {
return verror.New(errEncodeFailed, nil, err)
}
return nil
}