Update with optimistic locking

main
Drew Bednar 5 hours ago
parent 83a6e1a1f7
commit 25189e1c5b

@ -32,3 +32,11 @@ curl -i -X POST -d '{"title":"Death of a Unicorn","year":2025,"runtime":"126 min
BODY='{"title":"Black Panther","year":2018,"runtime":"134 mins","genres":["sci-fi","action","adventure"]}'
curl -X PUT -d "$BODY" http://0.0.0.0:5002/v1/movies/1
```
## Generating an Update Conflict
We can try to use the xargs tool to submit multiple requests to our webserver. Hopefully this results in the 409 that we are looking to create.
```bash
xargs -I % -P8 curl -X PATCH -d '{"runtime": "97 mins"}' "localhost:5002/v1/movies/4" < <(printf '%s\n' {1..8})
```

@ -64,3 +64,8 @@ func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Reques
func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
}
func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) {
message := "unable to update the record due to an edit conflict, please review new version and try again"
app.errorResponse(w, r, http.StatusConflict, message)
}

@ -122,10 +122,19 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
return
}
// var input struct {
// Title string `json:"title"`
// Year int32 `json:"year"`
// Runtime data.Runtime `json:"runtime"`
// Genres []string `json:"genres"`
// }
// Choosing to use pointers to allow us to determine if the zero value was sent
// by the client or if it was excluded from the client submission
var input struct {
Title string `json:"title"`
Year int32 `json:"year"`
Runtime data.Runtime `json:"runtime"`
Title *string `json:"title"`
Year *int32 `json:"year"`
Runtime *data.Runtime `json:"runtime"`
Genres []string `json:"genres"`
}
@ -135,10 +144,19 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
return
}
movie.Title = input.Title
movie.Year = input.Year
movie.Runtime = input.Runtime
if input.Title != nil {
movie.Title = *input.Title
}
if input.Year != nil {
movie.Year = *input.Year
}
if input.Runtime != nil {
movie.Runtime = *input.Runtime
}
if input.Genres != nil {
movie.Genres = input.Genres
}
v := validator.New()
@ -149,7 +167,12 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
err = app.models.Movies.Update(r.Context(), movie)
if err != nil {
switch {
case errors.Is(err, data.ErrEditConflict):
app.editConflictResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}

@ -25,7 +25,7 @@ func (app *application) routes() http.Handler {
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthCheckHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.getMovieHandler)
router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
// middleware

@ -8,6 +8,7 @@ import (
var (
ErrRecordNotFound = errors.New("record not found")
ErrEditConflict = errors.New("edit conflict")
)
// Models is a wrapper around all model repository objects

@ -81,10 +81,17 @@ func (m MovieModel) Get(ctx context.Context, id int64) (*Movie, error) {
}
func (m MovieModel) Update(ctx context.Context, movie *Movie) error {
// Using version here as an optimistic lock. Look up optimistic vs pessimistic locking
// https://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking/129397#129397
// version incrementing with an integer is cheap. Don't try with a last_updated timestamp...that could
// cause issues. If you don't want version to be guessable then a UUID generated by the DB is suitable.
// Example: SET ... version = uuid_generate_v4()
query := `
UPDATE movies
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
WHERE id = $5
WHERE id = $5 and version = $6
RETURNING version
`
@ -94,9 +101,21 @@ func (m MovieModel) Update(ctx context.Context, movie *Movie) error {
movie.Runtime,
movie.Genres,
movie.ID,
movie.Version,
}
return m.pool.QueryRow(ctx, query, args...).Scan(&movie.Version)
// Will not return any rows if the version number has already changed.
err := m.pool.QueryRow(ctx, query, args...).Scan(&movie.Version)
if err != nil {
switch {
case errors.Is(err, pgx.ErrNoRows):
return ErrEditConflict
default:
return err
}
}
return nil
}
// Here I decided to attempt an idiomatic version of the Delete service with

Loading…
Cancel
Save