Skip to content
Open
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
1 change: 1 addition & 0 deletions backend/bootstrap/framework.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
$settings->get('global.debugMode'),
$settings->get('global.productionEnv')
);
$mysqlDB->setDeltaTracking($settings->get('transactions.deltaConjunctMaintenance', 'off') !== 'off');
$ampersandApp->setDefaultStorage($mysqlDB);
$ampersandApp->setConjunctCache(new MysqlConjunctCache($mysqlDB));

Expand Down
19 changes: 18 additions & 1 deletion backend/src/Ampersand/Core/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,14 @@ class Relation
* Contains information about mysql table and columns in which this relation is administrated
*/
private MysqlDBRelationTable $mysqlTable;


/**
* Name of the table that holds this relation's touched pairs during a
* transaction, for delta-scoped re-evaluation (issue Ampersand#1684).
* Null when the compiler did not emit one.
*/
protected ?string $deltaTable = null;

/**
* Constructor
*/
Expand All @@ -113,6 +120,8 @@ public function __construct(array $relationDef, LoggerInterface $logger, Ampersa
$this->logger = $logger;
$this->app = $app;

$this->deltaTable = $relationDef['deltaTable'] ?? null;

$this->name = $relationDef['name'];
$this->srcConcept = $app->getModel()->getConcept($relationDef['srcConceptName']);
$this->tgtConcept = $app->getModel()->getConcept($relationDef['tgtConceptName']);
Expand Down Expand Up @@ -177,6 +186,14 @@ public function getMysqlTable(): MysqlDBRelationTable
return $this->mysqlTable;
}

/**
* Delta table for this relation, or null when the compiler did not emit one
*/
public function getDeltaTable(): ?string
{
return $this->deltaTable;
}

/**
* Get registered plugs for this relation
*
Expand Down
7 changes: 7 additions & 0 deletions backend/src/Ampersand/Misc/defaultSettings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ settings:
### Transaction settings
transactions.ignoreInvariantViolations: false # for debugging can be set to true (transactions will be committed regardless off invariant violations)
transactions.skipUniInjConjuncts: false # TODO: remove after fix for issue #535
# Delta-scoped re-evaluation of conjunct violations (issue Ampersand#1684).
# 'off' = full re-evaluation as always; 'shadow' = maintain the cache with the
# delta protocol AND evaluate in full, log any difference, full result stays
# authoritative; 'on' = delta protocol maintains the cache, full evaluation
# only for conjuncts outside the supported class. Requires generics produced
# by a compiler that emits deltaQueries; without those the setting is a no-op.
transactions.deltaConjunctMaintenance: 'off'
transactions.skipCleanConjuncts: false # skip the close's re-evaluation of conjuncts that were evaluated in this transaction with no mutations registered afterwards (issue #443)
transactions.interfaceAutoSaveChanges: true # specifies whether changes in interface are directly communicated (saved) to server

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ class MysqlConjunctCache implements CacheItemPoolInterface
/**
* Constructor
*/
public function getTableName(): string
{
return $this->tableName;
}

public function getDatabase(): MysqlDB
{
return $this->database;
}

public function __construct(MysqlDB $database, string $tableName = '__conj_violation_cache__')
{
$this->database = $database;
Expand Down
150 changes: 146 additions & 4 deletions backend/src/Ampersand/Plugs/MysqlDB/MysqlDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,39 @@
* Attribute is reset to 0 on start of (new) transaction
*/
protected int $queryCount = 0;

/**
* Specifies if touched pairs are recorded in the relations' delta tables
* (delta-scoped re-evaluation, issue Ampersand#1684). Enabled from
* bootstrap when transactions.deltaConjunctMaintenance is not 'off'.
*/
protected bool $deltaTracking = false;

/**
* Pairs recorded in delta tables within the open transaction, keyed by
* delta table name; per table a map "src|tgt" => [src, tgt] (DB
* representation). Used to dedupe inserts and to remove exactly these rows
* before commit, so concurrent transactions never touch each other's rows.
*
* @var array<string, array<string, array{0: string, 1: string}>>
*/
protected array $deltaRecordedPairs = [];

/**
* Touched relations within the open transaction: signature => delta table
*
* @var array<string, string>
*/
protected array $deltaTouchedRelations = [];

/**
* Relations that underwent a bulk mutation (deleteAllLinks/emptyRelation)
* in the open transaction; their conjuncts need full re-evaluation because
* the removed pairs are not individually recorded.
*
* @var array<string, true>
*/
protected array $deltaBulkMutated = [];

/**
* Constructor
Expand Down Expand Up @@ -394,11 +427,12 @@
public function commitTransaction(Transaction $transaction): void
{
$this->logger->info("Commit mysql database transaction for {$transaction}");
$this->cleanupDeltaTables(); // remove this transaction's delta rows before COMMIT, so they never leak
$this->execute("COMMIT");
$this->dbTransactionActive = false;
$this->logger->info("{$this->queryCount} queries executed in this transaction");
}

/**
* Function to rollback changes made in the open database transaction
*/
Expand All @@ -407,8 +441,104 @@
$this->logger->info("Rollback mysql database transaction for {$transaction}");
$this->execute("ROLLBACK");
$this->dbTransactionActive = false;
$this->clearDeltaAdministration(); // delta rows are rolled back with the transaction
$this->logger->info("{$this->queryCount} queries executed in this transaction");
}

/**
* Enable/disable recording of touched pairs in delta tables
*/
public function setDeltaTracking(bool $enabled): void
{
$this->deltaTracking = $enabled;
}

/**
* Record a touched pair in the relation's delta table (inside the open
* transaction). Deduplicated per transaction; the exact rows are removed
* again in cleanupDeltaTables() before commit.
*/
protected function recordDeltaPair(Relation $relation, string $srcAtomId, string $tgtAtomId): void

Check warning on line 461 in backend/src/Ampersand/Plugs/MysqlDB/MysqlDB.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 4 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=AmpersandTarski_Prototype&issues=AaBT42E9C6cPLGJLGFIQ&open=AaBT42E9C6cPLGJLGFIQ&pullRequest=449
{
if (!$this->deltaTracking) {
return;
}
// Only record inside an open DB transaction: a write in autocommit mode
// (e.g. the session's lastAccess update) commits immediately, so its
// delta row would outlive the request — the pre-commit cleanup never
// runs for it. Conjunct evaluation of such writes does not go through
// Transaction::close anyway.
if (!$this->dbTransactionActive) {
return;
}
$deltaTable = $relation->getDeltaTable();
if ($deltaTable === null) {
return;
}
$key = "{$srcAtomId}|{$tgtAtomId}";
if (isset($this->deltaRecordedPairs[$deltaTable][$key])) {
return;
}
$this->deltaRecordedPairs[$deltaTable][$key] = [$srcAtomId, $tgtAtomId];
$this->deltaTouchedRelations[$relation->signature] = $deltaTable;
// INSERT IGNORE: a leftover row (from an aborted run) only widens the
// candidate set, which stays correct; it must not break the insert.
$this->execute("INSERT IGNORE INTO \"{$deltaTable}\" (\"src\", \"tgt\") VALUES ('{$srcAtomId}', '{$tgtAtomId}')");
}

/**
* Mark a relation as bulk-mutated in the open transaction (its removed
* pairs are not individually recorded)
*/
protected function markDeltaBulkMutated(Relation $relation): void
{
if (!$this->deltaTracking) {
return;
}
$this->deltaBulkMutated[$relation->signature] = true;
}

/**
* Touched relations of the open transaction: signature => delta table name
*
* @return array<string, string>
*/
public function getDeltaTouchedRelations(): array
{
return $this->deltaTouchedRelations;
}

/**
* True when the relation underwent a bulk mutation in the open transaction
*/
public function isDeltaBulkMutated(string $relationSignature): bool
{
return isset($this->deltaBulkMutated[$relationSignature]);
}

/**
* Remove exactly the delta rows this transaction inserted, then forget the
* administration. Runs inside the open transaction (before COMMIT).
*/
protected function cleanupDeltaTables(): void
{
foreach ($this->deltaRecordedPairs as $deltaTable => $pairs) {
foreach ($pairs as [$srcAtomId, $tgtAtomId]) {
$this->execute("DELETE FROM \"{$deltaTable}\" WHERE \"src\" = '{$srcAtomId}' AND \"tgt\" = '{$tgtAtomId}'");
}
}
$this->clearDeltaAdministration();
}

/**
* Forget the per-transaction delta administration
*/
protected function clearDeltaAdministration(): void
{
$this->deltaRecordedPairs = [];
$this->deltaTouchedRelations = [];
$this->deltaBulkMutated = [];
}

/**************************************************************************************************
*
Expand Down Expand Up @@ -657,9 +787,11 @@
default:
throw new FatalException("Unsupported TableType '{$relTable->inTableOf()->value}' to addLink for for relation '{$relation}'");
}

// Check if query resulted in an affected row
$this->checkForAffectedRows();

$this->recordDeltaPair($relation, $srcAtomId, $tgtAtomId);
}

/**
Expand Down Expand Up @@ -697,8 +829,10 @@
default:
throw new FatalException("Unsupported TableType '{$relTable->inTableOf()->value}' to deleteLink for for relation '{$relation}'");
}

$this->checkForAffectedRows(); // Check if query resulted in an affected row

$this->recordDeltaPair($relation, $srcAtomId, $tgtAtomId);
}

/**
Expand Down Expand Up @@ -733,8 +867,12 @@
default:
throw new FatalException("Unsupported TableType '{$relationTable->inTableOf()->value}' to deleteAllLinks for for relation '{$relation}'");
}

$this->execute($query);

// The removed pairs are not individually recorded; conjuncts on this
// relation need full re-evaluation this transaction.
$this->markDeltaBulkMutated($relation);
}

/**
Expand All @@ -757,6 +895,10 @@
default:
throw new FatalException("Unknown 'tableOf' option for relation '{$relation}'");
}

// The removed pairs are not individually recorded; conjuncts on this
// relation need full re-evaluation this transaction.
$this->markDeltaBulkMutated($relation);
}

/**************************************************************************************************
Expand Down
Loading