Skip to content

Commit

Permalink
Merge pull request #13 from xoeye/bugfix/versioned-diffed-update-item…
Browse files Browse the repository at this point in the history
…-retries-transaction-conflicts

fix: versioned updates should also be resilient to concurrent transactions
  • Loading branch information
Peter Gaultney authored Feb 23, 2021
2 parents 172931e + 4639c2b commit 83a96cc
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 34 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 1.10.2

- Fixed `versioned_diffed_update_item` to support concurrent use with
transactions.

### 1.10.1

- Fixed bug in `versioned_transact_write_items` where un-effected
Expand Down
2 changes: 1 addition & 1 deletion xoto3/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""xoto3"""
__version__ = "1.10.1"
__version__ = "1.10.2"
__author__ = "Peter Gaultney"
__author_email__ = "[email protected]"
37 changes: 5 additions & 32 deletions xoto3/dynamodb/update/versioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
strongly_consistent_get_item_if_exists,
)
from xoto3.dynamodb.types import AttrDict, Item, ItemKey, TableResource
from xoto3.errors import client_error_name
from xoto3.dynamodb.write_versioned.ddb_api import (
is_cancelled_and_retryable,
versioned_item_expression,
)
from xoto3.utils.dt import iso8601strict
from xoto3.utils.tree_map import SimpleTransform

Expand Down Expand Up @@ -157,7 +160,7 @@ def versioned_diffed_update_item(
update_item(table, item_key, **update_arguments, **expr)
return updated_item
except ClientError as ce:
if client_error_name(ce) == "ConditionalCheckFailedException":
if is_cancelled_and_retryable(ce):
msg = (
"Attempt %d to update %s in table %s was beaten "
+ "by a different update. Sleeping for %s seconds."
Expand Down Expand Up @@ -187,36 +190,6 @@ def versioned_diffed_update_item(
)


# this could be used in a put_item scenario as well, or even with a batch_writer
def versioned_item_expression(
item_version: int, item_version_key: str = "item_version", id_that_exists: str = ""
) -> dict:
"""Assembles a DynamoDB ConditionExpression with ExprAttrNames and
Values that will ensure that you are the only caller of
versioned_item_diffed_update that has updated this item.
In general it would be a silly thing to not pass id_that_exists if
your item_version is not also 0. However, since this is just a
helper function and is only used (currently) by the local consumer
versioned_item_diffed_update, there is no need to enforce this.
"""
expr_names = {"#itemVersion": item_version_key}
expr_vals = {":curItemVersion": item_version}
item_version_condition = "#itemVersion = :curItemVersion"
first_time_version_condition = "attribute_not_exists(#itemVersion)"
if id_that_exists:
expr_names["#idThatExists"] = id_that_exists
first_time_version_condition = (
f"( {first_time_version_condition} AND attribute_exists(#idThatExists) )"
)
return dict(
ExpressionAttributeNames=expr_names,
ExpressionAttributeValues=expr_vals,
ConditionExpression=item_version_condition + " OR " + first_time_version_condition,
)


def make_prefetched_get_item(
item: Item,
refetch_getter: ItemGetter = strongly_consistent_get_item,
Expand Down
31 changes: 30 additions & 1 deletion xoto3/dynamodb/write_versioned/ddb_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing_extensions import Protocol, TypedDict

from xoto3.dynamodb.types import Item
from xoto3.dynamodb.update.versioned import versioned_item_expression
from xoto3.dynamodb.utils.serde import serialize_item
from xoto3.dynamodb.utils.table import table_primary_keys
from xoto3.errors import client_error_name
Expand Down Expand Up @@ -134,6 +133,36 @@ def _serialize_versioned_expr(expr: dict) -> dict:
return dict(expr, ExpressionAttributeValues=serialize_item(expr["ExpressionAttributeValues"]))


# this could be used in a put_item scenario as well, or even with a batch_writer
def versioned_item_expression(
item_version: int, item_version_key: str = "item_version", id_that_exists: str = ""
) -> dict:
"""Assembles a DynamoDB ConditionExpression with ExprAttrNames and
Values that will ensure that you are the only caller of
versioned_item_diffed_update that has updated this item.
In general it would be a silly thing to not pass id_that_exists if
your item_version is not also 0. However, since this is just a
helper function and is only used (currently) by the local consumer
versioned_item_diffed_update, there is no need to enforce this.
"""
expr_names = {"#itemVersion": item_version_key}
expr_vals = {":curItemVersion": item_version}
item_version_condition = "#itemVersion = :curItemVersion"
first_time_version_condition = "attribute_not_exists(#itemVersion)"
if id_that_exists:
expr_names["#idThatExists"] = id_that_exists
first_time_version_condition = (
f"( {first_time_version_condition} AND attribute_exists(#idThatExists) )"
)
return dict(
ExpressionAttributeNames=expr_names,
ExpressionAttributeValues=expr_vals,
ConditionExpression=item_version_condition + " OR " + first_time_version_condition,
)


def built_transaction_to_transact_write_items_args(
transaction: VersionedTransaction,
last_written_at_str: str,
Expand Down

0 comments on commit 83a96cc

Please sign in to comment.