diff options
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php')
-rw-r--r-- | www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php new file mode 100644 index 00000000..d98d6983 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php @@ -0,0 +1,275 @@ +<?php + +namespace SMW\MediaWiki\Deferred; + +use Closure; +use SMW\MediaWiki\Database; + +/** + * Extends DeferredCallableUpdate to allow handling of transaction related tasks + * or isolations to ensure an undisturbed update process before and after + * MediaWiki::preOutputCommit. + * + * @license GNU GPL v2+ + * @since 3.0 + * + * @author mwjames + */ +class TransactionalCallableUpdate extends CallableUpdate { + + /** + * @var Database|null + */ + private $connection; + + /** + * @var boolean + */ + private $onTransactionIdle = false; + + /** + * @var int|null + */ + private $transactionTicket = null; + + /** + * @var array + */ + private $preCommitableCallbacks = []; + + /** + * @var array + */ + private $postCommitableCallbacks = []; + + /** + * @var boolean + */ + private $autoCommit = false; + + /** + * @since 3.0 + * + * @param callable $callback|null + * @param Database|null $connection + */ + public function __construct( callable $callback = null, Database $connection ) { + parent::__construct( $callback ); + $this->connection = $connection; + $this->connection->onTransactionResolution( [ $this, 'cancelOnRollback' ], __METHOD__ ); + } + + /** + * @note MW 1.29+ showed transaction collisions (Exception thrown with + * an uncommitted database transaction), use 'onTransactionIdle' to isolate + * the update execution. + * + * @since 2.5 + */ + public function waitOnTransactionIdle() { + $this->onTransactionIdle = !$this->isCommandLineMode; + } + + /** + * @since 3.0 + */ + public function runAsAutoCommit() { + $this->autoCommit = true; + } + + /** + * It tries to fetch a transactionTicket to assert whether transaction writes + * are active or not and if available will process Database::commitAndWaitForReplication + * during DeferredCallableUpdate::doUpdate to safely post commits to the + * master. + * + * @note If the commandLine is active then continue an update without a ticket + * to avoid any update lag or possible transaction lock. + * + * @since 3.0 + */ + public function commitWithTransactionTicket() { + if ( $this->isCommandLineMode === false && $this->isDeferrableUpdate === true ) { + $this->transactionTicket = $this->connection->getEmptyTransactionTicket( $this->getOrigin() ); + } + } + + /** + * Attaches a callback pre-execution of the source callback and is scheduled + * to be executed before the source callback. + * + * @since 3.0 + * + * @param string $fname + * @param Closure $callback + */ + public function addPreCommitableCallback( $fname, callable $callback ) { + if ( is_callable( $callback ) ) { + $this->preCommitableCallbacks[$fname] = $callback; + } + } + + /** + * Attaches a callback post execution of the source callback and is scheduled + * to be executed after the source callback. + * + * @since 3.0 + * + * @param string $fname + * @param Closure $callback + */ + public function addPostCommitableCallback( $fname, callable $callback ) { + if ( is_callable( $callback ) ) { + $this->postCommitableCallbacks[$fname] = $callback; + } + } + + /** + * @see DeferrableUpdate::doUpdate + * + * @since 3.0 + */ + public function doUpdate() { + + if ( $this->onTransactionIdle ) { + return $this->runOnTransactionIdle(); + } + + $this->runPreCommitCallbacks(); + + $e = null; + $autoTrx = null; + + if ( $this->autoCommit ) { + $this->logger->info( [ 'DeferrableUpdate', 'Transactional, as auto commit', 'Update' ] ); + $autoTrx = $this->connection->getFlag( DBO_TRX ); + $this->connection->clearFlag( DBO_TRX ); + } + + try { + parent::doUpdate(); + } catch ( \Exception $e ) { + } + + if ( $this->autoCommit && $autoTrx ) { + $this->connection->setFlag( DBO_TRX ); + } + + if ( $e ) { + throw $e; + } + + $this->runPostCommitCallbacks(); + + if ( $this->transactionTicket !== null ) { + $this->connection->commitAndWaitForReplication( $this->getOrigin(), $this->transactionTicket ); + } + } + + /** + * @since 3.0 + */ + public function cancelOnRollback( $trigger ) { + if ( $trigger === Database::TRIGGER_ROLLBACK ) { + $this->callback = [ $this, 'emptyCancelCallback' ]; + } + } + + protected function addUpdate( $update ) { + + if ( $this->onTransactionIdle ) { + $this->logger->info( + [ + 'DeferrableUpdate', + 'Transactional', + 'Added: {origin} (onTransactionIdle)' + ], + [ + 'method' => __METHOD__, + 'role' => 'developer', + 'origin' => $this->getOrigin() + ] + ); + + return $this->connection->onTransactionIdle( function() use( $update ) { + $update->onTransactionIdle = false; + parent::addUpdate( $update ); + } ); + } + + parent::addUpdate( $update ); + } + + protected function getLoggableContext() { + return parent::getLoggableContext() + [ + 'transactionTicket' => $this->transactionTicket + ]; + } + + private function runOnTransactionIdle() { + $this->connection->onTransactionIdle( function() { + $this->logger->info( + [ + 'DeferrableUpdate', + 'Transactional', + 'Update: {origin} (onTransactionIdle)' + ], + [ + 'method' => __METHOD__, + 'role' => 'developer', + 'origin' => $this->getOrigin() + ] + ); + $this->onTransactionIdle = false; + $this->doUpdate(); + } ); + } + + private function runPreCommitCallbacks() { + foreach ( $this->preCommitableCallbacks as $fname => $preCallback ) { + $this->logger->info( + [ + 'DeferrableUpdate', + 'Transactional', + 'Update: {origin} (pre-commitable callback: {fname})' + ], + [ + 'method' => __METHOD__, + 'role' => 'developer', + 'origin' => $this->getOrigin(), + 'fname' => $fname + ] + ); + + call_user_func( $preCallback, $this->transactionTicket ); + } + } + + private function runPostCommitCallbacks() { + foreach ( $this->postCommitableCallbacks as $fname => $postCallback ) { + $this->logger->info( + [ + 'DeferrableUpdate', + 'Transactional', + 'Update: {origin} (post-commitable callback: {fname})' + ], + [ + 'method' => __METHOD__, + 'role' => 'developer', + 'origin' => $this->getOrigin(), + 'fname' => $fname + ] + ); + + call_user_func( $postCallback, $this->transactionTicket ); + } + } + + protected function emptyCancelCallback() { + $this->logger->info( + [ 'DeferrableUpdate', 'cancelOnRollback' ], + [ 'role' => 'developer', 'method' => __METHOD__ ] + ); + } + +} |