From e8f994965d00cc4db1dac85a74687fa5c6a531c0 Mon Sep 17 00:00:00 2001
From: Alex Miller <alexmiller@apple.com>
Date: Thu, 29 Mar 2018 18:00:12 -0700
Subject: [PATCH] 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.
---
 bindings/python/fdb/impl.py     | 21 +++++++++++++--------
 bindings/python/tests/tester.py | 10 ++++++++++
 2 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/bindings/python/fdb/impl.py b/bindings/python/fdb/impl.py
index 77c7a74d13..e69bda22cb 100644
--- a/bindings/python/fdb/impl.py
+++ b/bindings/python/fdb/impl.py
@@ -22,16 +22,17 @@
 
 import ctypes
 import ctypes.util
+import datetime
 import functools
+import inspect
+import multiprocessing
+import os
+import platform
+import sys
 import threading
 import traceback
-import inspect
-import datetime
-import platform
-import os
-import sys
-import multiprocessing
 
+import fdb
 from fdb import six
 
 _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
     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
     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:
                         yield asyncio.From(tr.on_error(e.code))
         else:
-
             @functools.wraps(func)
             def wrapper(*args, **kwargs):
                 if isinstance(args[index], TransactionRead):
@@ -269,6 +271,9 @@ def transactional(*tr_args, **tr_kwargs):
                     except FDBError as e:
                         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()
                     # td = now - last
                     # elapsed = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
diff --git a/bindings/python/tests/tester.py b/bindings/python/tests/tester.py
index 32ae2c01a3..c430d699f9 100644
--- a/bindings/python/tests/tester.py
+++ b/bindings/python/tests/tester.py
@@ -125,6 +125,16 @@ class Instruction:
         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):
     db.options.set_max_watches(100001)
     db.options.set_datacenter_id("dc_id")