Repository: edgedb/edgedb

Issue:
Debuggability for tx serialization errors
<!-- Please search existing issues to avoid creating duplicates. -->

<!-- Describe the feature you'd like to see implemented in EdgeDB. -->
Our schema/queries seem to result in a vastly disproportionate number of transaction serialization errors. Virtually all our queries are standalone and not in explicit transactions. I understand there are optimizations we can make and in extreme cases file bugs with you to make compiler improvements but this is currently surprisingly not very actionable:

Because our first and foremost priority is the queries actually working in production, so we have an extremely high maximum retries of 10 using the default TS client backoff function. This intentionally obscures many would-be errors. Eg, if something is retried even 5 times, there is already probably something wrong.

We only ever see errors in the cases where it fails 10 times. Even then there is no guarantee that the particular query that failed is the one to optimize and not something that was competing in the background. It is also currently not practical to correlate running queries with the output of the backoff function.

I propose that edgedb support logging the content of queries that are retried, or perhaps retried more than a given number of times. This would let us get ahead of tx serialization problems.


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/normalization.py b/edb/edgeql/compiler/normalization.py
--- a/edb/edgeql/compiler/normalization.py
+++ b/edb/edgeql/compiler/normalization.py
@@ -278,7 +278,7 @@ def normalize_InsertQuery(
         localnames=localnames,
     )
 
-    for field in ('shape',):
+    for field in ('shape', 'unless_conflict',):
         value = getattr(node, field, None)
         _normalize_recursively(
             node,
diff --git a/edb/edgeql/tracer.py b/edb/edgeql/tracer.py
--- a/edb/edgeql/tracer.py
+++ b/edb/edgeql/tracer.py
@@ -1142,6 +1142,10 @@ def trace_SortExpr(node: qlast.SortExpr, *, ctx: TracerContext) -> None:
 @trace.register
 def trace_InsertQuery(node: qlast.InsertQuery, *, ctx: TracerContext) -> None:
     with alias_context(ctx, node.aliases) as ctx:
+        if node.unless_conflict:
+            trace(node.unless_conflict[0], ctx=ctx)
+            trace(node.unless_conflict[1], ctx=ctx)
+
         tip = trace(qlast.Path(steps=[node.subject]), ctx=ctx)
         _update_path_prefix(tip, ctx=ctx)
 


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -3891,6 +3891,52 @@ def test_schema_trigger_02(self):
 
         self._assert_migration_consistency(schema)
 
+    def test_schema_trigger_03(self):
+        schema = '''
+            type User {
+              trigger logInsert after insert for each do (
+                insert Log {
+                  user := __new__,
+                  action := 'Insert',
+                } unless conflict on .action else (
+                  update Log set { user := __new__ }
+                )
+              );
+            }
+
+            type Log {
+              required link user -> User;
+              required property action -> str { constraint exclusive; }
+            }
+        '''
+
+        self._assert_migration_consistency(schema)
+
+    def test_schema_trigger_04(self):
+        schema = '''
+            type User {
+              trigger logInsert after insert for each do (
+                insert Log {
+                  user := __new__,
+                  action := 'Insert',
+                } unless conflict on .action else (
+                  insert BackupLog { user := __new__, action := '???' }
+                )
+              );
+            }
+
+            type Log {
+              required link user -> User;
+              required property action -> str { constraint exclusive; }
+            }
+            type BackupLog {
+              required link user -> User;
+              required property action -> str;
+            }
+        '''
+
+        self._assert_migration_consistency(schema)
+
     def test_schema_globals_funcs_01(self):
         schema = '''
             required global x1 -> int64 { default := 0 };