python: Post-API Version 620, @fdb.transactional on a generator will throw.

Previously, writing code like

    @fdb.transactional
    def foo(tr):
      yield tr.get('a')
      yield tr.get('b')

    print(foo(db))

was accepted by the python bindings, but had surprising semantics.  The
function returned by @fdb.transactional opens a transaction, runs foo(),
commits the transaction, and then returns the generator returned by foo().
This generator then uses the committed transaction.  This worked before API
version 410 (FDB 4.1), and hasn't worked since.  This will also be a problem if
a closure is returned from foo() that contains `tr`, but it's much harder to
check that in Python.

Rather than allow users to hit an unexpected and mysterious "Operation issued
while a commit was outstanding" exception, it's nicer to explicitly highlight
this problem as soon as we can.  Unfortunately, we have no way to know that a
function will return a generator until we call it, so that's the soonest we can
give a more informative error.
This commit is contained in:
Alex Miller 2018-03-29 18:00:12 -07:00
parent 6f52059750
commit e8f994965d
2 changed files with 23 additions and 8 deletions

View File

@ -22,16 +22,17 @@
import ctypes import ctypes
import ctypes.util import ctypes.util
import datetime
import functools import functools
import inspect
import multiprocessing
import os
import platform
import sys
import threading import threading
import traceback import traceback
import inspect
import datetime
import platform
import os
import sys
import multiprocessing
import fdb
from fdb import six from fdb import six
_network_thread = None _network_thread = None
@ -203,7 +204,9 @@ def transactional(*tr_args, **tr_kwargs):
It is important to note that the wrapped method may be called It is important to note that the wrapped method may be called
multiple times in the event of a commit failure, until the commit multiple times in the event of a commit failure, until the commit
succeeds. succeeds. This restriction requires that the wrapped function
may not be a generator, or a function that returns a closure that
contains the `tr` object.
If given a Transaction, the Transaction will be passed into the If given a Transaction, the Transaction will be passed into the
wrapped code, and WILL NOT be committed at completion of the wrapped code, and WILL NOT be committed at completion of the
@ -247,7 +250,6 @@ def transactional(*tr_args, **tr_kwargs):
except FDBError as e: except FDBError as e:
yield asyncio.From(tr.on_error(e.code)) yield asyncio.From(tr.on_error(e.code))
else: else:
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if isinstance(args[index], TransactionRead): if isinstance(args[index], TransactionRead):
@ -269,6 +271,9 @@ def transactional(*tr_args, **tr_kwargs):
except FDBError as e: except FDBError as e:
tr.on_error(e.code).wait() tr.on_error(e.code).wait()
if fdb.get_api_version() >= 620 and isinstance(ret, types.GeneratorType):
raise ValueError("Generators can not be wrapped with fdb.transactional")
# now = datetime.datetime.now() # now = datetime.datetime.now()
# td = now - last # td = now - last
# elapsed = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6) # elapsed = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)

View File

@ -125,6 +125,16 @@ class Instruction:
self.stack.push(self.index, val) self.stack.push(self.index, val)
def test_fdb_transactional_generator(db):
try:
@fdb.transactional
def function_that_yields(tr):
yield 0
assert fdb.get_api_version() < 620, "Generators post-6.2.0 should throw"
except ValueError as e:
pass
def test_db_options(db): def test_db_options(db):
db.options.set_max_watches(100001) db.options.set_max_watches(100001)
db.options.set_datacenter_id("dc_id") db.options.set_datacenter_id("dc_id")