3 ms·
When I worked at my last company, we had to expose an easy-to-use API for an optimistic-concurrency database that required cooperating clients to retry operatio
by itp 11y ago
When I worked at my last company, we had to expose an easy-to-use API for an optimistic-concurrency database that required cooperating clients to retry operations in a variety of situations. Rather than asking every client to implement correct retry behavior, we tried to expose a simple retry loop in an idiomatic way in each client language.
Python was far and away my favorite, since we could take advantage of decorators to make everything relatively seamless.
Consider a function something like this:
def do_something_transactionally(db, *args, **kwargs):
tr = db.create_transaction()
# do some things with the transaction object
tr.commit().get()
This code was broken in a number of ways -- there's no retry, or handling of errors from commit, etc.
So we exposed a decorator in Python to handle all of these issues for the common case:
@transactional
def do_something_transactionally(db, *args, **kwargs):
# do some things with the transaction object
This was more or less equivalent to:
def do_something_transactionally(db, *args, **kwargs):
tr = db.create_transaction()
while True:
try:
# do some things with the transaction object
tr.commit().wait()
break
except Error as e:
tr.on_error(e).get()
It was actually even niftier than that, though, because the decorator allowed you to pass in either a transaction object or a database object. If the caller provided a transaction rather than a database, then the decorator did not create a new transaction or commit it; this allowed composition of more complicated transactional calls of decorated functions.
So I will definitely grant you that if you encountered this code, you would need to understand what @transactional meant, but I would definitely argue that this was cleaner and easier to deal with than asking every function to explicitly implement the retry logic.
- matthiasv 11y agoThat looks actually more like a use case for a context manager, i.e. with transactional as transaction: # do something It's pretty clear at the call site what is and what isn't transactional and most importantly is practically the pythonic way of writing code with guaranteed cleanup.
- itp 11y agoWe really wanted to support a context manager there, believe me. IIRC the problem was there is/was no facility for retry with a context manager, so there would still need to be structure around it. I could be wrong, though -- I only remember being seduced by the context manager option a couple times before running into the same wall. I don't think the context manager, even if it had been doable, would have supported composition in the style of: @transactional def thing_one(tr, *args): # stuff @transactional def thing_two(tr, *args): # other stuff @transactional def both_things(tr, *args): thing_one(tr, args) thing_two(tr, args) If it's not clear, in this case a caller could provide a database object to thing_one or thing_two and they would be executed as a single atomic transaction, but a call to both_things with a database object would execute both thing_one and thing_two inside of a single atomic transaction.
- icebraining 11y agoYou're not wrong, it was an explicit decision: https://mail.python.org/pipermail/python-ideas/2013-May/020633.html https://mail.python.org/pipermail/python-ideas/2013-May/0206...