Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions drivine/backend/oracle/oracle_schema_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,50 @@ def recorded_as(kind: CatalogKind) -> list[str]:
"""


BUSY_ATTEMPTS = 300
"""
How many times `_made` runs an index's statement that another session's lock on the node table turned away (ORA-00054),
a tenth of a second apart, before it raises: as drivine4j's `OracleGraph.BUSY_ATTEMPTS`.
"""

_WAITS_OUT_BUSY = "PRAGMA EXCEPTION_INIT(e_busy, -54);"
"""What declares the busy lock `_made` waits out: a block that holds it has waited before it fails for that lock."""


def waits_out_busy_lock(statement: str) -> bool:
"""
Whether `statement` waited out another session's busy lock (ORA-00054) itself before it failed for it, as a block of
`_made` does for thirty seconds: the schema executor doesn't run it again for that lock, or each run would wait as
long again.
"""
return _WAITS_OUT_BUSY in statement


def _made(ddl: str) -> str:
"""A block that runs `ddl`, an object another session made meanwhile (ORA-00955) taken as made."""
"""
A block that runs `ddl`, an index over the node table, an object another session made meanwhile (ORA-00955) taken
as made. A statement turned away because another session holds the node table's lock (ORA-00054), as one making the
same index, or writing a node, does, is run again a tenth of a second later, for up to `BUSY_ATTEMPTS` tries, and
then finds what the other made: managers making the store key's constraint together otherwise ran out of attempts
on the busy lock, and went on without the constraint. drivine4j's `OracleGraph.ensuring` waits out a busy lock so.
"""
return f"""DECLARE
e_exists EXCEPTION;
PRAGMA EXCEPTION_INIT(e_exists, -955);
e_busy EXCEPTION;
{_WAITS_OUT_BUSY}
BEGIN
EXECUTE IMMEDIATE {sql_text(ddl)};
EXCEPTION
WHEN e_exists THEN NULL;
FOR attempt IN 1 .. {BUSY_ATTEMPTS} LOOP
BEGIN
EXECUTE IMMEDIATE {sql_text(ddl)};
RETURN;
EXCEPTION
WHEN e_exists THEN RETURN;
WHEN e_busy THEN
IF attempt = {BUSY_ATTEMPTS} THEN RAISE; END IF;
DBMS_SESSION.SLEEP(0.1);
END;
END LOOP;
END;"""


Expand Down
16 changes: 11 additions & 5 deletions drivine/schema/schema_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,27 @@
from typing import Any

from drivine.backend.cypher_grammar import FALKORDB_GRAMMAR, NEO4J_GRAMMAR
from drivine.backend.oracle.oracle_schema_grammar import waits_out_busy_lock
from drivine.backend.postgres.postgres_statement import value_of
from drivine.connection.connection_provider import ConnectionProvider
from drivine.connection.database_type import DatabaseType
from drivine.drivine_error import DrivineError
from drivine.manager.contention import any_cause, ddl_contended, when_not_contended
from drivine.manager.contention import any_cause, ddl_contended, ddl_deadlocked, when_not_contended
from drivine.query.query_specification import QuerySpecification
from drivine.schema.native_operation import NativeOperation

_ORACLE_SERIAL = re.compile(r"ORA-08177\b")


def _contended(failure: BaseException) -> bool:
def _contended(failure: BaseException, waited_out_busy: bool) -> bool:
"""
A transient error over Bolt, FalkorDB turning a write away while another is in progress, an Oracle serialization
failure, or a SQL store turning DDL away because of another session's (see ddl_contended), as when managers that
start together each make the indexes of the store's key.
start together each make the indexes of the store's key. A statement that `waited_out_busy`, as an Oracle index's
block does (see waits_out_busy_lock), is taken as turned away only for a deadlock (see ddl_deadlocked).
"""
return ddl_contended(failure) or any_cause(
ddl = ddl_deadlocked(failure) if waited_out_busy else ddl_contended(failure)
return ddl or any_cause(
failure,
lambda it: (
NEO4J_GRAMMAR.contended(it) or FALKORDB_GRAMMAR.contended(it) or bool(_ORACLE_SERIAL.search(str(it)))
Expand All @@ -42,7 +45,10 @@ async def run(self, statement: str | NativeOperation, bindings: Mapping[str, Any
"""
Runs a statement, again after a pause when the engine turned it away because another write was in progress.
"""
return await when_not_contended(lambda: self._run_once(statement, bindings or {}), _contended)
waited_out_busy = isinstance(statement, str) and waits_out_busy_lock(statement)
return await when_not_contended(
lambda: self._run_once(statement, bindings or {}), lambda it: _contended(it, waited_out_busy)
)

async def _run_once(self, statement: str | NativeOperation, bindings: Mapping[str, Any]) -> list[Any]:
if isinstance(statement, NativeOperation):
Expand Down
16 changes: 11 additions & 5 deletions drivine/sync/schema/schema_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@
from typing import Any

from drivine.backend.cypher_grammar import FALKORDB_GRAMMAR, NEO4J_GRAMMAR
from drivine.backend.oracle.oracle_schema_grammar import waits_out_busy_lock
from drivine.backend.postgres.postgres_statement import value_of
from drivine.connection.database_type import DatabaseType
from drivine.drivine_error import DrivineError
from drivine.query.query_specification import QuerySpecification
from drivine.schema.native_operation import NativeOperation
from drivine.sync.connection.connection_provider import ConnectionProvider
from drivine.sync.manager.contention import any_cause, ddl_contended, when_not_contended
from drivine.sync.manager.contention import any_cause, ddl_contended, ddl_deadlocked, when_not_contended

_ORACLE_SERIAL = re.compile(r"ORA-08177\b")


def _contended(failure: BaseException) -> bool:
def _contended(failure: BaseException, waited_out_busy: bool) -> bool:
"""
A transient error over Bolt, FalkorDB turning a write away while another is in progress, an Oracle serialization
failure, or a SQL store turning DDL away because of another session's (see ddl_contended), as when managers that
start together each make the indexes of the store's key.
start together each make the indexes of the store's key. A statement that `waited_out_busy`, as an Oracle index's
block does (see waits_out_busy_lock), is taken as turned away only for a deadlock (see ddl_deadlocked).
"""
return ddl_contended(failure) or any_cause(
ddl = ddl_deadlocked(failure) if waited_out_busy else ddl_contended(failure)
return ddl or any_cause(
failure,
lambda it: (
NEO4J_GRAMMAR.contended(it) or FALKORDB_GRAMMAR.contended(it) or bool(_ORACLE_SERIAL.search(str(it)))
Expand All @@ -44,7 +47,10 @@ def run(self, statement: str | NativeOperation, bindings: Mapping[str, Any] | No
"""
Runs a statement, again after a pause when the engine turned it away because another write was in progress.
"""
return when_not_contended(lambda: self._run_once(statement, bindings or {}), _contended)
waited_out_busy = isinstance(statement, str) and waits_out_busy_lock(statement)
return when_not_contended(
lambda: self._run_once(statement, bindings or {}), lambda it: _contended(it, waited_out_busy)
)

def _run_once(self, statement: str | NativeOperation, bindings: Mapping[str, Any]) -> list[Any]:
if isinstance(statement, NativeOperation):
Expand Down
2 changes: 1 addition & 1 deletion tests/golden/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
32b7497ad14ce26d40058037da16b89542562540
c34cf55178cf5971c4963e5a0f2445b581d469a7
Loading
Loading