ref/services/groups: adding leveldb-based persistent layer implementation

Change-Id: Ia802b8fd50eabe9eb67f10cf8f8a8a1c94484be3
diff --git a/services/groups/groupsd/main.go b/services/groups/groupsd/main.go
index f37c64a..a1ef298 100644
--- a/services/groups/groupsd/main.go
+++ b/services/groups/groupsd/main.go
@@ -23,7 +23,7 @@
 	"v.io/x/ref/lib/xrpc"
 	_ "v.io/x/ref/runtime/factories/roaming"
 	"v.io/x/ref/services/groups/internal/server"
-	"v.io/x/ref/services/groups/internal/store/memstore"
+	"v.io/x/ref/services/groups/internal/store/mem"
 )
 
 var flagName string
@@ -68,7 +68,7 @@
 		ctx.Infof("No permissions flag provided. Giving local principal all permissions.")
 		perms = defaultPerms(security.DefaultBlessingPatterns(v23.GetPrincipal(ctx)))
 	}
-	m := server.NewManager(memstore.New(), perms)
+	m := server.NewManager(mem.New(), perms)
 	server, err := xrpc.NewDispatchingServer(ctx, flagName, m)
 	if err != nil {
 		fmt.Errorf("NewDispatchingServer(%v) failed: %v", flagName, err)
diff --git a/services/groups/internal/server/server_test.go b/services/groups/internal/server/server_test.go
index 11d81b1..1a68b70 100644
--- a/services/groups/internal/server/server_test.go
+++ b/services/groups/internal/server/server_test.go
@@ -23,10 +23,19 @@
 	"v.io/x/ref/services/groups/internal/server"
 	"v.io/x/ref/services/groups/internal/store"
 	"v.io/x/ref/services/groups/internal/store/gkv"
-	"v.io/x/ref/services/groups/internal/store/memstore"
+	"v.io/x/ref/services/groups/internal/store/leveldb"
+	"v.io/x/ref/services/groups/internal/store/mem"
 	"v.io/x/ref/test/testutil"
 )
 
+type backend int
+
+const (
+	gkvstore backend = iota
+	leveldbstore
+	memstore
+)
+
 func Fatal(t *testing.T, args ...interface{}) {
 	debug.PrintStack()
 	t.Fatal(args...)
@@ -89,20 +98,18 @@
 	return reflect.DeepEqual(a, b)
 }
 
-// TODO(sadovsky): Write storage engine tests, then maybe drop this constant.
-const useMemstore = false
-
-func newServer(ctx *context.T) (string, func()) {
+func newServer(ctx *context.T, be backend) (string, func()) {
 	// TODO(sadovsky): Pass in perms and test perms-checking in Group.Create().
 	perms := access.Permissions{}
 	var st store.Store
-	var file *os.File
+	var path string
 	var err error
 
-	if useMemstore {
-		st = memstore.New()
-	} else {
-		file, err = ioutil.TempFile("", "")
+	switch be {
+	case memstore:
+		st = mem.New()
+	case gkvstore:
+		file, err := ioutil.TempFile("", "")
 		if err != nil {
 			ctx.Fatal("ioutil.TempFile() failed: ", err)
 		}
@@ -110,6 +117,18 @@
 		if err != nil {
 			ctx.Fatal("gkv.New() failed: ", err)
 		}
+		path = file.Name()
+	case leveldbstore:
+		path, err = ioutil.TempDir("", "")
+		if err != nil {
+			ctx.Fatal("ioutil.TempDir() failed: ", err)
+		}
+		st, err = leveldb.Open(path)
+		if err != nil {
+			ctx.Fatal("leveldb.Open() failed: ", err)
+		}
+	default:
+		ctx.Fatal("unknown backend: ", be)
 	}
 
 	m := server.NewManager(st, perms)
@@ -122,13 +141,13 @@
 	name := server.Status().Endpoints[0].Name()
 	return name, func() {
 		server.Stop()
-		if file != nil {
-			os.Remove(file.Name())
+		if path != "" {
+			os.RemoveAll(path)
 		}
 	}
 }
 
-func setupOrDie() (clientCtx *context.T, serverName string, cleanup func()) {
+func setupOrDie(be backend) (clientCtx *context.T, serverName string, cleanup func()) {
 	ctx, shutdown := v23.Init()
 	cp, sp := testutil.NewPrincipal("client"), testutil.NewPrincipal("server")
 
@@ -156,7 +175,7 @@
 		clientCtx.Fatal("v23.WithPrincipal() failed: ", err)
 	}
 
-	serverName, stopServer := newServer(serverCtx)
+	serverName, stopServer := newServer(serverCtx, be)
 	cleanup = func() {
 		stopServer()
 		shutdown()
@@ -167,8 +186,20 @@
 ////////////////////////////////////////
 // Test cases
 
-func TestCreate(t *testing.T) {
-	ctx, serverName, cleanup := setupOrDie()
+func TestCreateGkvStore(t *testing.T) {
+	testCreateHelper(t, gkvstore)
+}
+
+func TestCreateLevelDBStore(t *testing.T) {
+	testCreateHelper(t, leveldbstore)
+}
+
+func TestCreateMemStore(t *testing.T) {
+	testCreateHelper(t, memstore)
+}
+
+func testCreateHelper(t *testing.T, be backend) {
+	ctx, serverName, cleanup := setupOrDie(be)
 	defer cleanup()
 
 	// Create a group with a default perms and no entries.
@@ -219,8 +250,20 @@
 	}
 }
 
-func TestDelete(t *testing.T) {
-	ctx, serverName, cleanup := setupOrDie()
+func TestDeleteGkvStore(t *testing.T) {
+	testDeleteHelper(t, gkvstore)
+}
+
+func TestDeleteLevelDBStore(t *testing.T) {
+	testDeleteHelper(t, leveldbstore)
+}
+
+func TestDeleteMemStore(t *testing.T) {
+	testDeleteHelper(t, memstore)
+}
+
+func testDeleteHelper(t *testing.T, be backend) {
+	ctx, serverName, cleanup := setupOrDie(be)
 	defer cleanup()
 
 	// Create a group with a default perms and no entries, check that we can
@@ -279,8 +322,20 @@
 	}
 }
 
-func TestPerms(t *testing.T) {
-	ctx, serverName, cleanup := setupOrDie()
+func TestPermsGkvStore(t *testing.T) {
+	testPermsHelper(t, gkvstore)
+}
+
+func TestPermsLevelDBStore(t *testing.T) {
+	testPermsHelper(t, leveldbstore)
+}
+
+func TestPermsMemStore(t *testing.T) {
+	testPermsHelper(t, memstore)
+}
+
+func testPermsHelper(t *testing.T, be backend) {
+	ctx, serverName, cleanup := setupOrDie(be)
 	defer cleanup()
 
 	// Create a group with a default perms.
@@ -384,9 +439,21 @@
 	}
 }
 
-// Mirrors TestRemove.
-func TestAdd(t *testing.T) {
-	ctx, serverName, cleanup := setupOrDie()
+func TestAddGkvStore(t *testing.T) {
+	testAddHelper(t, gkvstore)
+}
+
+func TestAddLevelDBStore(t *testing.T) {
+	testAddHelper(t, leveldbstore)
+}
+
+func TestAddMemStore(t *testing.T) {
+	testAddHelper(t, memstore)
+}
+
+// testAddHelper tests mirror testRemoveHelper tests.
+func testAddHelper(t *testing.T, be backend) {
+	ctx, serverName, cleanup := setupOrDie(be)
 	defer cleanup()
 
 	// Create a group with a default perms and no entries.
@@ -469,9 +536,21 @@
 	}
 }
 
-// Mirrors TestAdd.
-func TestRemove(t *testing.T) {
-	ctx, serverName, cleanup := setupOrDie()
+func TestRemoveGkvStore(t *testing.T) {
+	testRemoveHelper(t, gkvstore)
+}
+
+func TestRemoveLevelDBStore(t *testing.T) {
+	testRemoveHelper(t, leveldbstore)
+}
+
+func TestRemoveMemStore(t *testing.T) {
+	testRemoveHelper(t, memstore)
+}
+
+// testRemoveHelper tests mirror testAddHelper tests.
+func testRemoveHelper(t *testing.T, be backend) {
+	ctx, serverName, cleanup := setupOrDie(be)
 	defer cleanup()
 
 	// Create a group with a default perms and two entries.
diff --git a/services/groups/internal/store/leveldb/store.go b/services/groups/internal/store/leveldb/store.go
new file mode 100644
index 0000000..5f38f52
--- /dev/null
+++ b/services/groups/internal/store/leveldb/store.go
@@ -0,0 +1,134 @@
+// 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.
+
+// Package leveldb provides an implementation of the groups server
+// Store interface that uses the levelDB-based syncbase storage layer.
+package leveldb
+
+import (
+	"strconv"
+
+	istore "v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/syncbase/x/ref/services/syncbase/store/leveldb"
+	"v.io/v23/vdl"
+	"v.io/v23/verror"
+	"v.io/v23/vom"
+	"v.io/x/ref/services/groups/internal/store"
+)
+
+type entry struct {
+	Value   interface{}
+	Version uint64
+}
+
+type T struct {
+	db istore.Store
+}
+
+var _ store.Store = (*T)(nil)
+
+// Open opens a groups server store located at the given path,
+// creating it if it doesn't exist.
+func Open(path string) (store.Store, error) {
+	db, err := leveldb.Open(path)
+	if err != nil {
+		return nil, convertError(err)
+	}
+	return &T{db: db}, nil
+}
+
+func (st *T) Get(key string, valbuf interface{}) (version string, err error) {
+	e, err := get(st.db, key)
+	if err != nil {
+		return "", err
+	}
+	if err := vdl.Convert(valbuf, e.Value); err != nil {
+		return "", convertError(err)
+	}
+	return strconv.FormatUint(e.Version, 10), nil
+}
+
+func (st *T) Insert(key string, value interface{}) error {
+	return istore.RunInTransaction(st.db, func(db istore.StoreReadWriter) error {
+		if _, err := get(db, key); verror.ErrorID(err) != store.ErrUnknownKey.ID {
+			if err != nil {
+				return err
+			}
+			return verror.New(store.ErrKeyExists, nil, key)
+		}
+		return put(db, key, &entry{Value: value})
+	})
+}
+
+func (st *T) Update(key string, value interface{}, version string) error {
+	return istore.RunInTransaction(st.db, func(db istore.StoreReadWriter) error {
+		e, err := get(db, key)
+		if err != nil {
+			return err
+		}
+		if err := e.checkVersion(version); err != nil {
+			return err
+		}
+		return put(db, key, &entry{Value: value, Version: e.Version + 1})
+	})
+}
+
+func (st *T) Delete(key string, version string) error {
+	return istore.RunInTransaction(st.db, func(db istore.StoreReadWriter) error {
+		e, err := get(db, key)
+		if err != nil {
+			return err
+		}
+		if err := e.checkVersion(version); err != nil {
+			return err
+		}
+		return delete(db, key)
+	})
+}
+
+func (st *T) Close() error {
+	return convertError(st.db.Close())
+}
+
+func get(db istore.StoreReadWriter, key string) (*entry, error) {
+	bytes, _ := db.Get([]byte(key), nil)
+	if bytes == nil {
+		return nil, verror.New(store.ErrUnknownKey, nil, key)
+	}
+	e := &entry{}
+	if err := vom.Decode(bytes, e); err != nil {
+		return nil, convertError(err)
+	}
+	return e, nil
+}
+
+func put(db istore.StoreReadWriter, key string, e *entry) error {
+	bytes, err := vom.Encode(e)
+	if err != nil {
+		return convertError(err)
+	}
+	if err := db.Put([]byte(key), bytes); err != nil {
+		return convertError(err)
+	}
+	return nil
+}
+
+func delete(db istore.StoreReadWriter, key string) error {
+	if err := db.Delete([]byte(key)); err != nil {
+		return convertError(err)
+	}
+	return nil
+}
+
+func (e *entry) checkVersion(version string) error {
+	newVersion := strconv.FormatUint(e.Version, 10)
+	if version != newVersion {
+		return verror.NewErrBadVersion(nil)
+	}
+	return nil
+}
+
+func convertError(err error) error {
+	return verror.Convert(verror.IDAction{}, nil, err)
+}
diff --git a/services/groups/internal/store/memstore/store.go b/services/groups/internal/store/mem/store.go
similarity index 92%
rename from services/groups/internal/store/memstore/store.go
rename to services/groups/internal/store/mem/store.go
index c086c44..f8ddd49 100644
--- a/services/groups/internal/store/memstore/store.go
+++ b/services/groups/internal/store/mem/store.go
@@ -2,10 +2,10 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package memstore provides a simple, in-memory implementation of server.Store.
-// Since it's a prototype implementation, it doesn't bother with entry-level
-// locking.
-package memstore
+// Package mem provides a simple, in-memory implementation of
+// server.Store. Since it's a prototype implementation, it doesn't
+// bother with entry-level locking.
+package mem
 
 import (
 	"strconv"