Skip to content
This repository has been archived by the owner on Apr 5, 2023. It is now read-only.

feat: Implement CouchDB Store #7

Merged
merged 1 commit into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,16 @@ module github.com/trustbloc/edge-core

go 1.13

require github.com/stretchr/testify v1.4.0
require (
github.com/flimzy/diff v0.1.6 // indirect
github.com/flimzy/testy v0.1.16 // indirect
github.com/go-kivik/couchdb v2.0.0+incompatible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it's incompatible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have a choice. The go-kivik module has the same and (i believe) go mod tidy will put it back if you try to take it out.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fqutishat The "incompatible" thing has something to do with the way the go modules are set up in the Go Kivik library. I don't think I can do anything about it until they update it. According to what I've read it shouldn't have any impact on us though. See https://stackoverflow.com/questions/57355929/what-incompatible-in-go-mod-mean-will-it-make-harm for more info

The author of the Go Kivik repo has an open issue for go.mod support, so I think when that's resolved then this "incompatible" thing will go away (after we update the dependency of course): go-kivik/kivik#417

github.com/go-kivik/kivik v2.0.0+incompatible
github.com/go-kivik/kiviktest v2.0.0+incompatible // indirect
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
gitlab.com/flimzy/testy v0.0.2 // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
)
40 changes: 40 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flimzy/diff v0.1.6 h1:ufTsTKcDtlaczpJTo3u1NeYqzuP6oRpy1VwQUIrgmBY=
github.com/flimzy/diff v0.1.6/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
github.com/flimzy/testy v0.1.16 h1:nchF7XYCkfHJiZKMRhAVKQp8jzpXFPwJYnSrnFysqlI=
github.com/flimzy/testy v0.1.16/go.mod h1:3szguN8NXqgq9bt9Gu8TQVj698PJWmyx/VY1frwwKrM=
github.com/go-kivik/couchdb v2.0.0+incompatible h1:DsXVuGJTng04Guz8tg7jGVQ53RlByEhk+gPB/1yo3Oo=
github.com/go-kivik/couchdb v2.0.0+incompatible/go.mod h1:5XJRkAMpBlEVA4q0ktIZjUPYBjoBmRoiWvwUBzP3BOQ=
github.com/go-kivik/kivik v2.0.0+incompatible h1:/7hgr29DKv/vlaJsUoyRlOFq0K+3ikz0wTbu+cIs7QY=
github.com/go-kivik/kivik v2.0.0+incompatible/go.mod h1:nIuJ8z4ikBrVUSk3Ua8NoDqYKULPNjuddjqRvlSUyyQ=
github.com/go-kivik/kiviktest v2.0.0+incompatible h1:y1RyPHqWQr+eFlevD30Tr3ipiPCxK78vRoD3o9YysjI=
github.com/go-kivik/kiviktest v2.0.0+incompatible/go.mod h1:JdhVyzixoYhoIDUt6hRf1yAfYyaDa5/u9SDOindDkfQ=
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d h1:vr95xIx8Eg3vCzZPxY3rCwTfkjqNDt/FgVqTOk0WByk=
github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc=
github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gitlab.com/flimzy/testy v0.0.2 h1:wii65HpZEbstqGT44+msiwzrX7SaxqNXj0BFjJc9iUY=
gitlab.com/flimzy/testy v0.0.2/go.mod h1:YObF4cq711ubd/3U0ydRQQVz7Cnq/ChgJpVwNr/AJac=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
Expand Down
204 changes: 204 additions & 0 deletions pkg/storage/couchdb/couchdbstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package couchdbstore

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"sync"

_ "github.com/go-kivik/couchdb" // The CouchDB driver
"github.com/go-kivik/kivik"

"github.com/trustbloc/edge-core/pkg/storage"
)

// Provider represents an CouchDB implementation of the storage.Provider interface
type Provider struct {
hostURL string
couchDBClient *kivik.Client
dbs map[string]*CouchDBStore
mux sync.RWMutex
}

const (
blankHostErrMsg = "hostURL for new CouchDB provider can't be blank"
failToCloseProviderErrMsg = "failed to close provider"
)

// NewProvider instantiates Provider
func NewProvider(hostURL string) (*Provider, error) {
if hostURL == "" {
return nil, errors.New(blankHostErrMsg)
}

client, err := kivik.New("couch", hostURL)
if err != nil {
return nil, err
}

return &Provider{hostURL: hostURL, couchDBClient: client, dbs: map[string]*CouchDBStore{}}, nil
}

// CreateStore creates a new store with the given name.
func (p *Provider) CreateStore(name string) error {
p.mux.Lock()
defer p.mux.Unlock()

err := p.couchDBClient.CreateDB(context.Background(), name)

return err
}

// OpenStore opens an existing store with the given name and returns it.
func (p *Provider) OpenStore(name string) (storage.Store, error) {
p.mux.Lock()
defer p.mux.Unlock()

// Check cache first
cachedStore, existsInCache := p.dbs[name]
if existsInCache {
return cachedStore, nil
}

// If it's not in the cache, then let's ask the CouchDB server if it exists
existsOnServer, err := p.couchDBClient.DBExists(context.Background(), name)
if err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be a race condition in which two clients are attempting to create the same DB. At the time you check if the DB exists it may not be there but then when you try to create it you'll get an error because it exists. You'll need to check again if the error means that the DB is there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code has been changed so now there are separate create store and open store methods (per our discussion with @fqutishat on Friday), so the DB exists check is no longer there. Let me know if you still think there's a race condition.

}

if !existsOnServer {
return nil, storage.ErrStoreNotFound
}

db := p.couchDBClient.DB(context.Background(), name)

// db.Err() won't return an error if the database doesn't exist, hence the need for the explicit DBExists call above
if dbErr := db.Err(); dbErr != nil {
return nil, dbErr
}

store := &CouchDBStore{db: db}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the store exists in cache then why not return it instead of creating a new one every time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


p.dbs[name] = store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need a write lock here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking through the OpenStore method, I think that I actually need a write lock for the whole method since it first checks to see if it exists, and then opens the store in the map. Let me know if you think I'm locking more than I need to here.


return store, nil
}

// CloseStore closes a previously opened store.
func (p *Provider) CloseStore(name string) error {
p.mux.Lock()
defer p.mux.Unlock()

store, exists := p.dbs[name]
if !exists {
return storage.ErrStoreNotFound
}

delete(p.dbs, name)

return store.db.Close(context.Background())
}

// Close closes the provider.
func (p *Provider) Close() error {
p.mux.Lock()
defer p.mux.Unlock()

for _, store := range p.dbs {
err := store.db.Close(context.Background())
if err != nil {
return fmt.Errorf(failToCloseProviderErrMsg+": %w", err)
}
}

return p.couchDBClient.Close(context.Background())
}

// CouchDBStore represents a CouchDB-backed database.
type CouchDBStore struct {
db *kivik.DB
}

// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
var valueToPut []byte
if isJSON(v) {
valueToPut = v
} else {
valueToPut = wrapTextAsCouchDBAttachment(v)
}

_, err := c.db.Put(context.Background(), k, valueToPut)
if err != nil {
return err
}

return nil
}

func isJSON(textToCheck []byte) bool {
var js json.RawMessage
return json.Unmarshal(textToCheck, &js) == nil
}

// Kivik has a PutAttachment method, but it requires creating a document first and then adding an attachment after.
// We want to do it all in one step, hence this manual stuff below.
func wrapTextAsCouchDBAttachment(textToWrap []byte) []byte {
encodedTextToWrap := base64.StdEncoding.EncodeToString(textToWrap)
return []byte(`{"_attachments": {"data": {"data": "` + encodedTextToWrap + `", "content_type": "text/plain"}}}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will these need indexing?

Copy link
Collaborator Author

@DRK3 DRK3 Feb 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, they shouldn't. This whole attachment thing is just to allow the CouchDB store to work with non-JSON payloads. The primary use case for the CouchDB store is to store EDV EncryptedDocuments, which are JSON. Those EncryptedDocuments will need indexing, but they won't be stored as attachments, so we should be okay.

}

// Get retrieves the value in the store associated with the given key.
func (c *CouchDBStore) Get(k string) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you retrieve value from couchdb you don't need to return the whole doc just return doc["data"]

destinationData := make(map[string]interface{})

row := c.db.Get(context.Background(), k)

err := row.ScanDoc(&destinationData)
if err != nil {
if err.Error() == "Not Found: missing" {
return nil, storage.ErrValueNotFound
}

return nil, err
}

_, containsAttachment := destinationData["_attachments"]
if containsAttachment {
return c.getDataFromAttachment(k)
}

// Stripping out the CouchDB-specific fields
delete(destinationData, "_id")
delete(destinationData, "_rev")

strippedJSON, err := json.Marshal(destinationData)
if err != nil {
return nil, err
}

return strippedJSON, nil
}

func (c *CouchDBStore) getDataFromAttachment(k string) ([]byte, error) {
// Original data was not JSON and so it was stored as an attachment
attachment, err := c.db.GetAttachment(context.Background(), k, "data")
if err != nil {
return nil, err
}

data, err := ioutil.ReadAll(attachment.Content)
if err != nil {
return nil, err
}

return data, nil
}
Loading