blob: d51cca1135c4e224dc243bb6f00c61fdecd6bfc1 [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/ioutil"
"os"
"reflect"
"sort"
"strings"
"sync"
"v.io/x/ref/services/profile"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/services/application"
"v.io/v23/verror"
"v.io/v23/vom"
)
// 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{:_}")
errCantOpenOrCreate = verror.Register(pkgPath+".errCantOpenOrCreate", verror.NoRetry, "{1:}{2:} File ({3}) could neither be opened ({4}) nor created ({5}){:_}")
errDecodeFailedFileMissing = verror.Register(pkgPath+".errDecodeFailedFileMissing", verror.NoRetry, "{1:}{2:} Decode() failed, file went missing{:_}")
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{:_}")
)
// 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"
)
var keyExists = struct{}{}
// TODO(ashankar,rjkroege): Once we switch from gob to vom, this hack won't be
// necessary (will be able to Put/Get application.Envelope without translating
// to this applicationEnvelope type). But in the mean time, required because
// security.Blessings is not gob-encodeable (and gob doesn't have the ability
// to convert between native and wire types like security.Blessings ->
// security.WireBlessings)
//
// This mirrors application.Envelope with the type of "Publsher" changed to
// security.WireBlessings.
//
// See https://vanadium-review.googlesource.com/6300
type applicationEnvelope struct {
Title string
Args []string
Binary application.SignedFile
Publisher security.WireBlessings
Env []string
Packages application.Packages
}
func translateToGobEncodeable(in interface{}) interface{} {
env, ok := in.(application.Envelope)
if !ok {
return in
}
return applicationEnvelope{
Title: env.Title,
Args: env.Args,
Binary: env.Binary,
Publisher: security.MarshalBlessings(env.Publisher),
Env: env.Env,
Packages: env.Packages,
}
}
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,
}, 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 != 6 {
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"))
}
}
// 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-gob")
if err != nil {
return nil, verror.New(errTempFileFailed, nil, os.TempDir(), "memstore-gob", err)
}
defer f.Close()
configuredPersistentFile = f.Name()
return &Memstore{
data: data,
persistedFile: configuredPersistentFile,
}, nil
}
file, err := os.Open(configuredPersistentFile)
if err != nil {
// The file doesn't exist. Attempt to create it instead.
file, cerr := os.Create(configuredPersistentFile)
if cerr != nil {
return nil, verror.New(errCantOpenOrCreate, nil, configuredPersistentFile, err, cerr)
}
defer file.Close()
} else {
decoder := gob.NewDecoder(file)
if err := decoder.Decode(&data); err != nil {
// Two situations. One is not an error.
fi, serr := os.Stat(configuredPersistentFile)
if serr != nil {
// Someone probably deleted the file out from underneath us. Give up.
return nil, verror.New(errDecodeFailedFileMissing, nil, serr)
}
if fi.Size() != 0 {
return nil, verror.New(errDecodeFailedBadData, nil, err)
}
// An empty backing file deserializes to an empty memstore.
}
}
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)
}
var err error
if o.Value, err = translateFromGobEncodeable(bv); err != nil {
return nil, err
}
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()")
}
envelope = translateToGobEncodeable(envelope)
switch v := envelope.(type) {
case applicationEnvelope, 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
set := 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]
}
set[name] = keyExists
}
}
if !found {
return nil, verror.New(verror.ErrNoExist, nil, o.path)
}
children := make([]string, len(set))
i := 0
for k, _ := range set {
children[i] = k
i++
}
sort.Strings(children)
return children, nil
}
// persist() writes the state of the Memstore to persistent storage.
func (ms *Memstore) persist() error {
file, err := os.Create(ms.persistedFile)
if err != nil {
return verror.New(errCreateFailed, nil, ms.persistedFile, err)
}
defer file.Close()
// TODO(rjkroege): Switch to the vom encoder.
enc := gob.NewEncoder(file)
err = enc.Encode(ms.data)
if err := enc.Encode(ms.data); err != nil {
return verror.New(errEncodeFailed, nil, err)
}
ms.clearTransactionState()
return nil
}