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

Conversation

DRK3
Copy link
Collaborator

@DRK3 DRK3 commented Feb 13, 2020

closes #4
Signed-off-by: Derek Trider [email protected]

@DRK3 DRK3 self-assigned this Feb 13, 2020
@cla-bot cla-bot bot added the cla-signed label Feb 13, 2020
@codecov
Copy link

codecov bot commented Feb 13, 2020

Codecov Report

Merging #7 into master will decrease coverage by 9.24%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff             @@
##            master       #7      +/-   ##
===========================================
- Coverage   100.00%   90.75%   -9.25%     
===========================================
  Files            1        2       +1     
  Lines           33      119      +86     
===========================================
+ Hits            33      108      +75     
- Misses           0        6       +6     
- Partials         0        5       +5     
Impacted Files Coverage Δ
pkg/storage/couchdb/couchdbstore.go 87.20% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2f206aa...42630ee. Read the comment docs.

@DRK3 DRK3 force-pushed the CouchDB branch 4 times, most recently from bf47165 to 7bda9e0 Compare February 13, 2020 22:13
@DRK3 DRK3 marked this pull request as ready for review February 13, 2020 22:13
@DRK3 DRK3 force-pushed the CouchDB branch 2 times, most recently from 4efa717 to b4b2524 Compare February 13, 2020 23:22
OpenStore(name string) (Store, error)

// CloseStore closes the store with the given name.
CloseStore(name string) error

// Close closes all stores created under this store provider.
Close() error

// StoreExists checks whether a store exists under the given name already.
StoreExists(name string) (bool, error)
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 If you recall, I had an issue in the EDV repo where I needed the ability to know if a store is open without actually opening it. The solution we had before was to track the open stores in a map in the operations.go file. However, I realized while working on this story that that strategy wouldn't work for a persistent store, since now there can be stores already created from running this executable previously. Thus, I need to ask the provider if a store is open first. So I added this "StoreExists" method to the Provider interface. Let me know if there's another way to do this.

Copy link
Contributor

@fqutishat fqutishat Feb 14, 2020

Choose a reason for hiding this comment

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

where you use Provider.StoreExists

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Per our discussion, I added the CreateStore method and made the OpenStore method only open existing stores.

}

// CloseStore closes a previously opened store.
// CouchDB and Kivik don't really have "close" mechanisms for individual databases, so there's nothing to do here.
Copy link
Collaborator Author

@DRK3 DRK3 Feb 13, 2020

Choose a reason for hiding this comment

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

This seems strange... but I couldn't find a way to close a CouchDB database. Is there a way?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this what you're looking for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@llorllale Yep, that's it. The older version of Kivik I used initially didn't have that call, so that's why I thought it didn't exist.

I updated the method.


"github.com/sirupsen/logrus"

"github.com/trustbloc/edge-core/pkg/storage"
Copy link
Contributor

Choose a reason for hiding this comment

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

wrong location

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

couchDBURL = "localhost:5984"
testStoreName = "teststore"
testKey = "sampleDBKey"
testValue = `{"JSONKey":"JSONValue"}`
Copy link
Contributor

Choose a reason for hiding this comment

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

you assume the value always JSON

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Per our discussion on Friday with @bstasyszyn , I updated the CouchDB store Put method so that a non-JSON payload will be saved as an attachment. I verified that JSON payloads do get merged with the CouchDB document (this should answer @bstasyszyn's question from Friday)

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


// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
_, err := c.db.Put(context.Background(), k, v)
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 to create doc and create key inside doc called data

Copy link
Contributor

Choose a reason for hiding this comment

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

doc := map[string]interface{}{
"_id": k,
"data":v
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we ever want to save the value as an attachment?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Per our discussion on Friday with @bstasyszyn , I updated the CouchDB store Put method so that a non-JSON payload will be saved as an attachment. I verified that JSON payloads do get merged with the CouchDB document (this should answer @bstasyszyn's question from Friday)


// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
_, err := c.db.Put(context.Background(), k, v)
Copy link
Contributor

Choose a reason for hiding this comment

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

}

// 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"]


// OpenStore opens and returns a store for the given name.
func (p *Provider) OpenStore(name string) (storage.Store, error) {
exists, err := p.couchDBClient.DBExists(context.Background(), name)
Copy link
Contributor

Choose a reason for hiding this comment

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

why are we still doing this? This stuff gave us lots of headaches in the past. The solution back then was to have a "leader" create the DB if it doesn't exist.

Why isn't this a devops issue?


// Close closes the provider.
func (p *Provider) Close() error {
p.couchDBClient = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@llorllale The older version of Kivik I used initially didn't have that call, so that's why I didn't use it before. I see the newer one does - I've updated the method.

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.

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.


err = p.couchDBClient.CreateDB(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.


// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
_, err := c.db.Put(context.Background(), k, v)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if it's a good idea to always use context.Background(). Usually it's a good idea to expose context in the API to allow the client to pass whatever type of context they want (i.e. with timeout, cancel, etc.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@bstasyszyn @fqutishat So would this mean adding a third argument to the Put() function for context? Or should I perhaps add it in to the Store/Provider struct and then grab the context whenever I need it? Just wondering what's the best way.

Copy link
Contributor

Choose a reason for hiding this comment

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

The context is for one time use, i.e. per request. So it would need to be in Put. The convention is to add it as the first arg.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Per our discussion, I created a follow-up issue for this: #8


// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
_, err := c.db.Put(context.Background(), k, v)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we ever want to save the value as an attachment?

@DRK3 DRK3 force-pushed the CouchDB branch 5 times, most recently from 1cbd5e6 to 0cd563e Compare February 14, 2020 22:44
exit 1
fi

DOCKER_RM_EXIT_CODE=0
Copy link
Contributor

Choose a reason for hiding this comment

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

duplicate code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I refactored the error code check into a separate function. Take a look and let me know what you think.

@fqutishat fqutishat self-requested a review February 18, 2020 05:20
// 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.


store := &CouchDBStore{db: db}

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.

}

func (p *Provider) storeExists(name string) (bool, error) {
_, existsInCache := p.dbs[name]
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a read lock. (Also in other places below.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since I added a write lock to the OpenStore method (which is the only place that calls the private method storeExists), I don't need to (and can't) add a read lock here. Let me know if this seems ok.

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.

@DRK3 DRK3 force-pushed the CouchDB branch 4 times, most recently from a60f216 to 599441b Compare February 18, 2020 21:53

// Put stores the given key-value pair in the store.
func (c *CouchDBStore) Put(k string, v []byte) error {
c.mux.Lock()
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need a lock here since you're accessing an immutable field.

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. (I also got rid of that line in the close method that clears the cache by remaking the map, as discussed)

func (c *CouchDBStore) Get(k string) ([]byte, error) {
destinationData := make(map[string]interface{})

c.mux.RLock()
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need a lock.

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.

}

// Check cache first
store, exists := p.dbs[name]
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems a bit awkward that you check if the store exists using storeExists and then you check again if it exists here. Maybe it would be better to get rid of storeExists, unless it's being used elsewhere?

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.

Signed-off-by: Derek Trider <[email protected]>
@fqutishat fqutishat merged commit a8e4e00 into trustbloc:master Feb 19, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement CouchDB store
4 participants