I have some TypeScript code in a project that does a number of native DynamoDB update operations:
import { nativeDocumentClient, nativeDynamo } from '../utils/aws';
// snipped code
// updatedProducts is of type `{ id: string; siteId: string; state: ProductState }[]`
await Promise.all(
updatedProducts.map(({ id, siteId, state }) =>
nativeDocumentClient
.update({
TableName,
Key: {
id,
siteId,
},
ExpressionAttributeNames: {
'#s': 'state',
},
ExpressionAttributeValues: {
':state': state,
':updatedAt': new Date().toISOString(),
},
UpdateExpression: 'SET #s = :state, updatedAt = :updatedAt',
ReturnValues: 'ALL_NEW',
})
.promise()
)
);
Now we would always expect this record (with its composite Key) to exist, but we have found a rare situation where this is not the case (it’s probably just poor data in a nonprod environment rather than a bug specifically). Unfortunately it looks like the underlying UpdateItemCommand
does an upsert and not an update, despite its name. From the official docs:
Edits an existing item’s attributes, or adds a new item to the table if it does not already exist
We can do a guard clause that does a get on the record, and skips the upsert if it does not exist. However that feels like an invitation for a race condition† – is there an option on .update()
that would get us what we want without separate conditionals?
† Update – a guard clause might look like this in pseudocode:
object = get(TableName, Key: {
id,
siteId,
})
// Do update only if the document exists
if (object) {
update(
TableName,
Key: {
id,
siteId,
},
...
)
}
I think this could be susceptible to a race condition because the read and write operations are not wrapped in any sort of transaction or lock.
2
Answers
I am adding a self-answer, as the other existing answer didn't work. We did try it, but it crashed in the case of the record not existing:
We solved it with a ternary short-circuit:
It has been correctly pointed out on this page that this could theoretically create a race condition, between detecting the presence of the object and it being deleted by another process; however, we don't delete this kind of document in our app. This solution doesn't crash in the case of an object not existing, but moreover we like it because it is easier to understand.
As an alternative, perhaps we could have stuck with the condition expression in Dynamo, but used a try catch to ignore
ConditionalCheckFailedException
throws.All writes/updates to DynamoDB are strongly serializable.
Using a
ConditionExpression
forattribute_exists()
would ensure you only update the item if it exists, if it does not exist the update will fail.