Skip to content

Instantly share code, notes, and snippets.

@numberoverzero
Last active January 9, 2019 09:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save numberoverzero/6e05979aa535f3fb4d967a80ac1ea994 to your computer and use it in GitHub Desktop.
Save numberoverzero/6e05979aa535f3fb4d967a80ac1ea994 to your computer and use it in GitHub Desktop.
demo of using bloop's transaction class
# Three-table solution to https://stackoverflow.com/q/35825034
# Generally this is a terrible idea, but it demos transactions (see the `new_user` method) and
# provides guaranteed uniqueness over a triplet of attributes.
# You could instead use optimistic writes (but be ready to clean up dangling records!), such as:
# (1) write Username if pk not exist, else fail
# (2) write Email if pk not exist, else roll back (1) and fail
# (3) write User if pk not exist, else roll back (1), (2) and fail
# (4) update (1), (2), (3) with attributes from each other so they are no longer dangling
# keep in mind that if the process crashes before (4) you'll still have records to clean up, so you need
# a low-velocity scan in the background. This is a great time to try out the Streams feature!
# https://bloop.readthedocs.io/en/latest/user/streams.html#retrieve-records
# If you decide to use a single table (eg. just the User class) you can't guarantee uniqueness,
# since a GSI is eventually consistent.
from bloop import BaseModel, Column, Condition, Engine, String
# https://bloop.readthedocs.io/en/latest/user/patterns.html#generic-if-not-exist
def if_not_exist(obj):
condition = Condition()
for key in obj.Meta.keys:
condition &= key.is_(None)
return condition
class Username(BaseModel):
username = Column(String, hash_key=True)
email = Column(String)
uid = Column(String)
class Email(BaseModel):
username = Column(String)
email = Column(String, hash_key=True)
uid = Column(String)
class User(BaseModel):
username = Column(String)
email = Column(String)
uid = Column(String, hash_key=True)
def fail_on(**kwargs):
"""helper to make a tx fail.
usage:
fail_on(email="TAKEN", uid="RESERVED")
# raises TransactionCanceled
new_user(email="TAKEN", username="unique", uid="also unique")
new_user(email="also unique", username="unique", uid="RESERVED")
"""
for x, cls in {"email": Email, "username": Username, "uid": User}.items():
if x in kwargs:
engine.save(cls(**kwargs))
def new_user(email, username, uid):
objs = [
cls(username=username, email=email, uid=uid)
for cls in {Username, Email, User}
]
with engine.transaction() as tx:
for obj in objs:
tx.save(obj, condition=if_not_exist(obj))
engine = Engine(table_name_template="tx.{table_name}")
engine.bind(BaseModel)
@numberoverzero
Copy link
Author

Here's a sample of using fail_on to prep an object in just the Email table:

In [1]: from bloop.exceptions import TransactionCanceled
In [2]: fail_on(email="TAKEN")
In [3]: try:
   ...:     new_user(email="TAKEN", username="numberoverzero", uid="123")
   ...: except TransactionCanceled:
   ...:     print("commit failed")
   ...:     
commit failed

@numberoverzero
Copy link
Author

Once you're done you can clean up the tx.{Email, User, Username} tables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment