Repository: edgedb/edgedb

Issue:
`session_idle_transaction_timeout` is racy with `START TRANSACTION`
- EdgeDB Version: 1.2, nightly
- OS Version: *

Steps to Reproduce (thanks @msullivan):

1. Connect in the CLI (`CONFIGURE SESSION SET session_idle_transaction_timeout := ` 5 minutes)
2. Run a query via one of the language bindings (reuse the same pgcon, reset `session_idle_transaction_timeout` to 10 seconds)
3. Start a transaction in the CLI; it will time out after 10 seconds (should have restored the state to 5 minutes)

See also: [this line in #2236](https://github.com/edgedb/edgedb/pull/2236/files#diff-b039a6bd14ce31ad50b05a56c6ec1fee36666ca1bd27f2089029dd6c42838210R1413).

~- [ ] Restore state after `START TRANSACTION` as a special case~
- [x] Use `READONLY` / `DEFERRABLE` transactions (implement if not sufficient) to write test cases to cover the regression fixed in #2236



Discussion:
Per @1st1 we can also add a Postgres `Sync` after state sync.  Though that'll probably require moving away from SimpleQuery altogether.
> Run a query via one of the language bindings (reuse the same pgcon, reset session_idle_transaction_timeout to 10 seconds)

FWIW this also works

`edb cli query "CONFIGURE SESSION SET session_idle_transaction_timeout := <duration>'5 seconds';"`

Resolving patch:
diff --git a/edb/edgeql/parser/grammar/statements.py b/edb/edgeql/parser/grammar/statements.py
--- a/edb/edgeql/parser/grammar/statements.py
+++ b/edb/edgeql/parser/grammar/statements.py
@@ -50,10 +50,6 @@ def reduce_ISOLATION_SERIALIZABLE(self, *kids):
         self.val = (qltypes.TransactionIsolationLevel.SERIALIZABLE,
                     kids[0].context)
 
-    def reduce_ISOLATION_REPEATABLE_READ(self, *kids):
-        self.val = (qltypes.TransactionIsolationLevel.REPEATABLE_READ,
-                    kids[0].context)
-
     def reduce_READ_WRITE(self, *kids):
         self.val = (qltypes.TransactionAccessMode.READ_WRITE,
                     kids[0].context)
diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py
--- a/edb/server/compiler/compiler.py
+++ b/edb/server/compiler/compiler.py
@@ -1356,8 +1356,6 @@ def _compile_ql_transaction(
             ctx.state.start_tx()
 
             sql = 'START TRANSACTION'
-            # N.B: Currently we ignore the isolation level. We should
-            # remove it if we keep it this way.
             if ql.access is not None:
                 sql += f' {ql.access.value}'
             if ql.deferrable is not None:
diff --git a/edb/tools/pygments/edgeql/meta.py b/edb/tools/pygments/edgeql/meta.py
--- a/edb/tools/pygments/edgeql/meta.py
+++ b/edb/tools/pygments/edgeql/meta.py
@@ -13,7 +13,6 @@ class EdgeQL:
         "__std__",
         "__subject__",
         "__type__",
-        "abort",
         "alter",
         "analyze",
         "and",
@@ -28,7 +27,6 @@ class EdgeQL:
         "configure",
         "create",
         "deallocate",
-        "declare",
         "delete",
         "describe",
         "detached",
@@ -37,7 +35,6 @@ class EdgeQL:
         "do",
         "drop",
         "else",
-        "empty",
         "end",
         "execute",
         "exists",
@@ -72,17 +69,13 @@ class EdgeQL:
         "on",
         "optional",
         "or",
-        "order",
         "over",
         "partition",
         "policy",
-        "populate",
         "prepare",
         "raise",
         "refresh",
         "reindex",
-        "release",
-        "reset",
         "revoke",
         "rollback",
         "select",
@@ -98,6 +91,7 @@ class EdgeQL:
         "with",
     )
     unreserved_keywords = (
+        "abort",
         "abstract",
         "after",
         "alias",
@@ -118,10 +112,12 @@ class EdgeQL:
         "current",
         "database",
         "ddl",
+        "declare",
         "deferrable",
         "deferred",
         "delegated",
         "desc",
+        "empty",
         "expression",
         "extension",
         "final",
@@ -147,9 +143,11 @@ class EdgeQL:
         "onto",
         "operator",
         "optionality",
+        "order",
         "overloaded",
         "owned",
         "package",
+        "populate",
         "postfix",
         "prefix",
         "property",
@@ -157,9 +155,11 @@ class EdgeQL:
         "pseudo",
         "read",
         "reject",
+        "release",
         "rename",
         "repeatable",
         "required",
+        "reset",
         "restrict",
         "role",
         "roles",


Test patch:
diff --git a/tests/test_edgeql_syntax.py b/tests/test_edgeql_syntax.py
--- a/tests/test_edgeql_syntax.py
+++ b/tests/test_edgeql_syntax.py
@@ -5614,11 +5614,9 @@ def test_edgeql_syntax_transaction_02(self):
         """
         START TRANSACTION ISOLATION SERIALIZABLE, READ ONLY, DEFERRABLE;
         START TRANSACTION ISOLATION SERIALIZABLE, READ ONLY;
-        START TRANSACTION ISOLATION REPEATABLE READ, READ ONLY;
         START TRANSACTION READ ONLY, DEFERRABLE;
         START TRANSACTION READ ONLY, NOT DEFERRABLE;
         START TRANSACTION READ WRITE, NOT DEFERRABLE;
-        START TRANSACTION ISOLATION REPEATABLE READ, READ WRITE;
         START TRANSACTION READ WRITE;
         """
 
@@ -5627,7 +5625,7 @@ def test_edgeql_syntax_transaction_02(self):
                   line=2, col=51)
     def test_edgeql_syntax_transaction_03(self):
         """
-        START TRANSACTION ISOLATION SERIALIZABLE, ISOLATION REPEATABLE READ;
+        START TRANSACTION ISOLATION SERIALIZABLE, ISOLATION SERIALIZABLE;
         """
 
     @tb.must_fail(errors.EdgeQLSyntaxError,
@@ -5646,6 +5644,13 @@ def test_edgeql_syntax_transaction_05(self):
         START TRANSACTION READ WRITE, DEFERRABLE, READ ONLY;
         """
 
+    def test_edgeql_syntax_transaction_06(self):
+        """
+        START TRANSACTION READ WRITE, NOT DEFERRABLE, ISOLATION SERIALIZABLE;
+% OK %
+        START TRANSACTION ISOLATION SERIALIZABLE, READ WRITE, NOT DEFERRABLE;
+        """
+
     def test_edgeql_syntax_describe_01(self):
         """
         DESCRIBE SCHEMA AS DDL;
diff --git a/tests/test_server_proto.py b/tests/test_server_proto.py
--- a/tests/test_server_proto.py
+++ b/tests/test_server_proto.py
@@ -1905,6 +1905,45 @@ async def test_server_proto_tx_20(self):
                 CREATE DATABASE t1;
             ''')
 
+    async def test_server_proto_tx_21(self):
+        # Test that the Postgres connection state is restored
+        # in a separate implicit transaction from where a
+        # START TRANSACTION is called.
+        #
+        # If state is restored in the same implicit transaction
+        # (between SYNCs) with starting a new DEFERRABLE transaction
+        # Postgres would complain that the transaction has already
+        # been started and it's too late to change it to DEFERRABLE.
+        #
+        # This test starts a separate cluster with a limited backend
+        # connections pool size, sets some session config aiming to
+        # set it on all backend connections in the pool, and then tries
+        # to run a transaction on a brand new EdgeDB connection.
+        # When that connection gets a PG connection from the backend pool,
+        # it must restore the state to "default". If it fails to inject
+        # a "SYNC" between state reset and "START TRANSACTION" this
+        # test would fail.
+
+        async with tb.start_edgedb_server(max_allowed_connections=4) as sd:
+            for _ in range(8):
+                con1 = await sd.connect()
+                try:
+                    await con1.query('SET ALIAS foo AS MODULE default')
+                finally:
+                    await con1.aclose()
+
+            con2 = await sd.connect()
+            try:
+                await con2.query('START TRANSACTION READ ONLY, DEFERRABLE')
+            finally:
+                await con2.aclose()
+
+            con2 = await sd.connect()
+            try:
+                await con2.execute('START TRANSACTION READ ONLY, DEFERRABLE')
+            finally:
+                await con2.aclose()
+
 
 class TestServerProtoMigration(tb.QueryTestCase):