Skip to content

Commit

Permalink
Add pessimistic locking, more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mramshaw committed Mar 12, 2019
1 parent 237a083 commit fce65ca
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 33 deletions.
3 changes: 2 additions & 1 deletion CURLs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ GET:

POST (Create):

curl -v -H "Content-Type: application/json" -d '{"name":"test recipe","preptime":1.11,"difficulty":1,"vegetarian":false}' localhost/v1/recipes
curl -v -H "Content-Type: application/json" -d '{"id":1,"name":"test recipe","preptime":1.11,"difficulty":1,"vegetarian":false}' localhost/v1/recipes
curl -v -H "Content-Type: application/json" -d '{"id":2,"name":"test recipe 2","preptime":1.22,"difficulty":2,"vegetarian":true}' localhost/v1/recipes

PUT (Update):

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ returning JSON documents.
Similiar to __redis__ and __Cassandra__, data may be assigned arbitrary
expiry times.

Couchbase offers both [optimistic and pessimistic locking](http://docs.couchbase.com/go-sdk/1.5/concurrent-mutations-cluster.html).

Couchbase is packaged with an Admin Console GUI.

#### Getting familiar with Couchbase
Expand Down Expand Up @@ -364,10 +366,15 @@ Query Optimization Using Prepared (Optimized) Statements:

http://docs.couchbase.com/go-sdk/1.5/n1ql-query.html#prepare-stmts

Concurrent Document Mutations

http://docs.couchbase.com/go-sdk/1.5/concurrent-mutations-cluster.html

## To Do

- [x] Learn [N1QL](http://docs.couchbase.com/server/6.0/getting-started/try-a-query.html)
- [ ] Investigate using views to enforce constraints (indexes are a performance nightmare)
- [x] Add Travis CI build process & code coverage reporting
- [x] Add pessimistic locking to updates
- [ ] Update build process to `vgo`
- [ ] Add tests for data TTL
14 changes: 11 additions & 3 deletions src/application/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ func (a *App) modifyRecipeEndpoint(w http.ResponseWriter, req *http.Request) {
}
defer req.Body.Close()
if err := r.UpdateRecipe(recipeID, a.DB); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
if gocb.IsKeyNotFoundError(err) {
respondWithError(w, http.StatusNotFound, err.Error())
} else {
respondWithError(w, http.StatusInternalServerError, err.Error())
}
return
}
respondWithJSON(w, http.StatusOK, r)
Expand Down Expand Up @@ -115,8 +119,12 @@ func (a *App) addRatingEndpoint(w http.ResponseWriter, req *http.Request) {
}
defer req.Body.Close()
if err := rr.AddRecipeRating(a.DB); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
if gocb.IsKeyNotFoundError(err) {
respondWithError(w, http.StatusNotFound, err.Error())
} else {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
}
respondWithJSON(w, http.StatusCreated, rr)
}
Expand Down
43 changes: 20 additions & 23 deletions src/recipes/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,21 @@ func (r *Recipe) GetRecipe(id string, db *gocb.Bucket) error {
// The recipe ID and the recipe ratings will not be changed.
func (r *Recipe) UpdateRecipe(id string, db *gocb.Bucket) error {

updateRecipeQuery := gocb.NewN1qlQuery("UPDATE recipes USE KEYS $1 SET name = $2, preptime = $3, difficulty = $4, vegetarian = $5").AdHoc(false)
var recipe Recipe

var params []interface{}
params = append(params, id)
params = append(params, r.Name)
params = append(params, r.PrepTime)
params = append(params, r.Difficulty)
params = append(params, r.Vegetarian)
// Get document, 3 second lock
cas, err := db.GetAndLock(id, 3, &recipe)
if err != nil {
return err
}

recipe.Name = r.Name
recipe.PrepTime = r.PrepTime
recipe.Difficulty = r.Difficulty
recipe.Vegetarian = r.Vegetarian

_, err := db.ExecuteN1qlQuery(updateRecipeQuery, params)
// Mutating unlocks the document
_, err = db.Replace(id, recipe, cas, 0)
if err != nil {
return err
}
Expand Down Expand Up @@ -178,30 +183,22 @@ func GetRecipesRated(db *gocb.Bucket, start int, count int, preptime float32) ([
// and the ratings are never overwritten.
func (rr *RecipeRating) AddRecipeRating(db *gocb.Bucket) error {

getRecipeQuery := gocb.NewN1qlQuery("SELECT ratings FROM recipes AS results USE KEYS $1").AdHoc(false)

id := strconv.Itoa(int(rr.RecipeID))

var params []interface{}
params = append(params, id)
var recipe Recipe

rs, err := db.ExecuteN1qlQuery(getRecipeQuery, params)
// Get document, 3 second lock
cas, err := db.GetAndLock(id, 3, &recipe)
if err != nil {
return err
}
defer rs.Close()

var row n1qlRatings
rs.One(&row)

ratings := row.Ratings
ratings := recipe.Ratings
ratings = append(ratings, rr.Rating)
recipe.Ratings = ratings

updateRecipeQuery := gocb.NewN1qlQuery("UPDATE recipes USE KEYS $1 SET ratings = $2").AdHoc(false)

params = append(params, ratings)

_, err = db.ExecuteN1qlQuery(updateRecipeQuery, params)
// Mutating unlocks the document
_, err = db.Replace(id, recipe, cas, 0)
if err != nil {
return err
}
Expand Down
84 changes: 78 additions & 6 deletions src/test/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import (
"gopkg.in/couchbase/gocb.v1"
)

const (
sleepTime = 6 // 6 seconds
)

var app application.App

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -63,6 +67,61 @@ func TestGetNonExistentRecipe(t *testing.T) {
}
}

func TestUpdatePutNonExistentRecipe(t *testing.T) {
clearTables()

payload := []byte(`{"name":"test recipe - put","preptime":11.11,"difficulty":3,"vegetarian":false}`)

req, err := http.NewRequest("PUT", "/v1/recipes/1", bytes.NewBuffer(payload))
if err != nil {
t.Errorf("Error on http.NewRequest (PUT): %s", err)
}
response := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response)

var m map[string]string
json.Unmarshal(response.Body.Bytes(), &m)
if m["error"] != "key not found" {
t.Errorf("Expected the 'error' of the response to be set to 'key not found'. Got '%s'", m["error"])
}
}

func TestUpdatePatchNonExistentRecipe(t *testing.T) {
clearTables()

payload := []byte(`{"name":"test recipe - patch","preptime":22.22,"difficulty":4,"vegetarian":false}`)

req, err := http.NewRequest("PATCH", "/v1/recipes/1", bytes.NewBuffer(payload))
if err != nil {
t.Errorf("Error on http.NewRequest (PATCH): %s", err)
}
response := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response)

var m map[string]string
json.Unmarshal(response.Body.Bytes(), &m)
if m["error"] != "key not found" {
t.Errorf("Expected the 'error' of the response to be set to 'key not found'. Got '%s'", m["error"])
}
}

func TestDeleteNonExistentRecipe(t *testing.T) {
clearTables()

req, err := http.NewRequest("DELETE", "/v1/recipes/1", nil)
if err != nil {
t.Errorf("Error on http.NewRequest (Second DELETE): %s", err)
}
response := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response)

var m map[string]string
json.Unmarshal(response.Body.Bytes(), &m)
if m["error"] != "key not found" {
t.Errorf("Expected the 'error' of the response to be set to 'key not found'. Got '%s'", m["error"])
}
}

func TestCreateRecipe(t *testing.T) {
clearTables()

Expand Down Expand Up @@ -315,12 +374,25 @@ func TestAddRating(t *testing.T) {
checkResponseCode(t, http.StatusCreated, response)
}

func TestAddRatingNonExistentRecipe(t *testing.T) {
clearTables()

payload := []byte(`{"rating":3}`)

req, err := http.NewRequest("POST", "/v1/recipes/1/rating", bytes.NewBuffer(payload))
if err != nil {
t.Errorf("Error on http.NewRequest (2nd POST): %s", err)
}
response := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response)
}

func TestGetRecipes(t *testing.T) {
clearTables()
addRecipes(1, 2)

// Sleep for 5 seconds to allow Couchbase time to commit
time.Sleep(5 * time.Second)
// Sleep the specified number of seconds to allow Couchbase time to commit
time.Sleep(sleepTime * time.Second)

var bb bytes.Buffer
mw := multipart.NewWriter(&bb)
Expand Down Expand Up @@ -376,8 +448,8 @@ func TestSearch(t *testing.T) {
response = executeRequest(req)
checkResponseCode(t, http.StatusCreated, response)

// Sleep for 5 seconds to allow Couchbase time to commit
time.Sleep(5 * time.Second)
// Sleep the specified number of seconds to allow Couchbase time to commit
time.Sleep(sleepTime * time.Second)

var bb bytes.Buffer
mw := multipart.NewWriter(&bb)
Expand Down Expand Up @@ -429,8 +501,8 @@ func TestSearch(t *testing.T) {

addRecipes(2, 12)

// Sleep for 5 seconds to allow Couchbase time to commit
time.Sleep(5 * time.Second)
// Sleep the specified number of seconds to allow Couchbase time to commit
time.Sleep(sleepTime * time.Second)

mw = multipart.NewWriter(&bb)
mw.WriteField("count", "20") // Should get reset to 10
Expand Down

0 comments on commit fce65ca

Please sign in to comment.