Skip to main content

What Is Optimistic Locking?

Optimistic locking is a strategy to avoid conflicts when multiple people may be editing a single record. Before committing a change, the system checks to make sure the record hasn't been updated before committing the update. For more information, check out the Wikipedia page and this MDN section on avoiding mid-air collisions.

How To Use Optimistic Locking (With E-Tags)

ETags (or entity tags) are an HTTP response header. An ETag is an identifier for a specific version of a resource. It lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content was not changed. Additionally, etags help to prevent simultaneous updates of a resource from overwriting each other. See references below for further information and related ADRs on why ETags are used on this project.

ETags are used in conjunction with the If-Match conditional HTTP request header. For GET and HEAD methods, the server will return the requested resource only if it matches one of the listed ETags. For PUT and other non-safe methods, it will only upload the resource in this case.

Note: you'll probably want to use this on PUT or PATCH endpoints only.

Leaning on the query builder

Let's say you're building out a new endpoint that needs optimistic locking to avoid people updating stale data. You have a handler and a service object, let's call them WidgetHandler and WidgetUpdater respectively. The WidgetUpdater is going to take care of the business logic. Let's take a look at the handler first:

type PatchWidgetHandler struct {
//...
services.WidgetUpdater
}


func (h PatchWidgetHandler) Handle(params widgetops.PatchWidgetParams) middleware.Responder {
//...

eTag := params.IfMatch

widget := h.UpdateWidget(someArg, eTag)

//...
}

We're grabbing the E-tag from the If-Match header, and passing it along to the service object. Meanwhile, in the service object:

type widgetUpdater struct {
//...
builder QueryBuilder
}

func (w *widgetUpdater) UpdateWidget(someArg interface{}, eTag string) {
var widget models.Widget
//...

verrs, err := w.builder.UpdateOne(&widget, &eTag)
}

type PreconditionFailedError struct {
id uuid.UUID
Err error
}

func (e PreconditionFailedError) Error() string {
return fmt.Sprintf("widget with id: '%s' could not be updated due to the record being stale", e.id.String())
}

You'll notice that UpdateOne, a function that takes a model struct as an interface{} in order to find the record and update it, now takes an optional string argument for an E-tag. If the supplied E-tag is stale, UpdateOne will return a query.StaleIdentifierError that you can then use to return a 412 Precondition Failed in the handler:

//...
widget, err := h.UpdateWidget(someArg, eTag)
if err != nil {
logger.Error("error: ", zap.Error(err))

switch e := err.(type) {
case widget.NotFoundError:
return widgetops.NewPatchWidgetNotFound()
case widget.PreconditionFailedError:
return widgetops.NewPatchWidgetPreconditionFailed()
default:
return widgetops.NewPatchWidgetInternalServerError()
}
}

return widgetops.NewPatchWidgetOk().WithPayload(widget) // make sure payload includes updated E-tag.
//...

How to use optimistic locking on the front-end

Luckily, once we've added the eTag key to our Swagger configuration, it's automatically stored in the Redux state. All we have to do is pull it out of the Redux state when we need it.

Let's say we need to make an update to an MTO's status. First, we'll need to visit the entity file:

// src/shared/Entities/modules/moveTaskOrders.js

//...


const updateMoveTaskOrders = 'moveTaskOrder.updateMoveTaskOrderStatus';
export function updateMoveTaskOrderStatus(
moveTaskOrderID,
isAvailableToPrime,
label = updateMoveTaskOrders,
) {
const swaggerTag = 'moveTaskOrder.updateMoveTaskOrderStatus';
return swaggerRequest(
getGHCClient,
swaggerTag,
{ moveTaskOrderID },
{ updateMoveTaskOrders },
);
}

//...

We need to make sure this function takes an ETag as an argument and passes it along in the If-Match header:

// src/shared/Entities/modules/moveTaskOrders.js

//...


const updateMoveTaskOrders = 'moveTaskOrder.updateMoveTaskOrderStatus';
export function updateMoveTaskOrderStatus(
moveTaskOrderID,
ifMatchETag,
isAvailableToPrime,
label = updateMoveTaskOrders,
) {
const swaggerTag = 'moveTaskOrder.updateMoveTaskOrderStatus';
return swaggerRequest(
getGHCClient,
swaggerTag,
{ moveTaskOrderID, 'If-Match': ifMatchETag },
{ updateMoveTaskOrders },
);
}

//...

Now we need to find where this function is actually being called. It looks like it's being used as the callback for a button click:

// src/scenes/Office/TOO/customerDetails.jsx

//...


<button
data-hi={moveTaskOrder.eTag}
onClick={() => this.props.updateMoveTaskOrderStatus(moveTaskOrder.id)}
>
Send to Prime
</button>

//...

We'll need to supply the ETag as an argument:

// src/scenes/Office/TOO/customerDetails.jsx

//...


<button
onClick={() => this.props.updateMoveTaskOrderStatus(moveTaskOrder.id, moveTaskOrder.eTag)}
>
Send to Prime
</button>

//...

You should now be able to update the move task order without getting that pesky 412 Precondition Failed error.

References