| Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Repository/TreeRepository.php |
<?php
namespace Flow\Repository;
use Flow\Data\BufferedCache;
use Flow\Data\ObjectManager;
use Flow\DbFactory;
use Flow\Model\UUID;
use BagOStuff;
use Flow\Exception\DataModelException;
/*
*
* In SQL
*
* CREATE TABLE flow_tree_node (
* descendant DECIMAL(39) UNSIGNED NOT NULL,
* ancestor DECIMAL(39) UNSIGNED NULL,
* depth SMALLINT UNSIGNED NOT NULL,
* PRIMARY KEY ( ancestor, descendant ),
* UNIQUE KEY ( descendant, depth )
* );
*
* In Memcache
*
* flow:tree:subtree:<descendant>
* flow:tree:rootpath:<descendant>
* flow:tree:parent:<descendant> - should we just use rootpath?
*
* Not sure how to handle topic splits with caching yet, i can imagine
* a number of potential race conditions for writing root paths and sub trees
* during a topic split
*/
class TreeRepository {
/**
* @var string
*/
protected $tableName = 'flow_tree_node';
/**
* @var DbFactory
*/
protected $dbFactory;
/**
* @var BufferedCache
*/
protected $cache;
/**
* @param DbFactory $dbFactory Factory to source connection objects from
* @param BufferedCache $cache
*/
public function __construct( DbFactory $dbFactory, BufferedCache $cache ) {
$this->dbFactory = $dbFactory;
$this->cache = $cache;
}
/**
* A helper function to generate cache keys for tree repository
* @param string $treeType
* @param UUID $uuid
* @return string
*/
protected function cacheKey( $treeType, UUID $uuid ) {
return TreeCacheKey::build( $treeType, $uuid );
}
/**
* Insert a new tree node. If ancestor === null then this node is a root.
*
* To also write this to cache we would have to read our own write, which
* isn't guaranteed during a node split. Master reads can potentially be
* a different server than master writes.
*
* The way to do it without that is to CAS update memcache, assuming it currently
* has what we need
*/
public function insert( UUID $descendant, UUID $ancestor = null ) {
$subtreeKey = $this->cacheKey( 'subtree', $descendant );
$parentKey = $this->cacheKey( 'parent', $descendant );
$pathKey = $this->cacheKey( 'rootpath', $descendant );
$this->cache->set( $subtreeKey, array( $descendant ) );
if ( $ancestor === null ) {
$this->cache->set( $parentKey, null );
$this->cache->set( $pathKey, array( $descendant ) );
$path = array( $descendant );
} else {
$this->cache->set( $parentKey, $ancestor );
$path = $this->findRootPath( $ancestor );
$path[] = $descendant;
$this->cache->set( $pathKey, $path );
}
$dbw = $this->dbFactory->getDB( DB_MASTER );
$res = $dbw->insert(
$this->tableName,
array(
'tree_descendant_id' => $descendant->getBinary(),
'tree_ancestor_id' => $descendant->getBinary(),
'tree_depth' => 0,
),
__METHOD__
);
if ( $res && $ancestor !== null ) {
try {
if ( defined( 'MW_PHPUNIT_TEST' ) && $dbw instanceof \DatabaseMysqlBase ) {
/*
* Combination of MW unit tests + MySQL DB is known to cause
* query failures of code 1137, so instead of executing a
* known bad query, let's just consider it failed right away
* (and let catch statement deal with it)
*/
throw new \DBQueryError( $dbw, 'Prevented execution of known bad query', 1137, '', __METHOD__ );
}
$res = $dbw->insertSelect(
$this->tableName,
$this->tableName,
array(
'tree_descendant_id' => $dbw->addQuotes( $descendant->getBinary() ),
'tree_ancestor_id' => 'tree_ancestor_id',
'tree_depth' => 'tree_depth + 1',
),
array(
'tree_descendant_id' => $ancestor->getBinary(),
),
__METHOD__
);
} catch( \DBQueryError $e ) {
$res = false;
/*
* insertSelect won't work on temporary tables (as used for MW
* unit tests), because it refers to the same table twice, in
* one query.
* In this case, we'll do a separate select & insert. This used
* to always be detected via the DBQueryError, but it can also
* return false from insertSelect.
*
* @see https://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html
* @see http://dba.stackexchange.com/questions/45270/mysql-error-1137-hy000-at-line-9-cant-reopen-table-temp-table
*/
if ( $e->errno === 1137 ) {
$res = true;
$rows = $dbw->select(
$this->tableName,
array( 'tree_depth', 'tree_ancestor_id' ),
array( 'tree_descendant_id' => $ancestor->getBinary() ),
__METHOD__
);
if ( $rows ) {
foreach ( $rows as $row ) {
$res &= $dbw->insert(
$this->tableName,
array(
'tree_descendant_id' => $descendant->getBinary(),
'tree_ancestor_id' => $row->tree_ancestor_id,
'tree_depth' => $row->tree_depth + 1,
),
__METHOD__
);
}
}
}
}
}
if ( !$res ) {
$this->cache->delete( $parentKey );
$this->cache->delete( $pathKey );
throw new DataModelException( 'Failed inserting new tree node', 'process-data' );
}
$this->appendToSubtreeCache( $descendant, $path );
return true;
}
protected function appendToSubtreeCache( UUID $descendant, array $rootPath ) {
$callback = function( BagOStuff $cache, $key, $value ) use( $descendant ) {
if ( $value === false ) {
return false;
}
$value[$descendant->getAlphadecimal()] = $descendant;
return $value;
};
// This could be pretty slow if there is contention
foreach ( $rootPath as $subtreeRoot ) {
$cacheKey = $this->cacheKey( 'subtree', $subtreeRoot );
$success = $this->cache->merge( $cacheKey, $callback );
// $success is always true if bufferCache starts with begin()
// if we failed to CAS new data, kill the cached value so it'll be
// re-fetched from DB
if ( !$success ) {
$this->cache->delete( $cacheKey );
}
}
}
/**
* Deletes a descendant from the tree repo.
*
* @param UUID $descendant
* @return bool
*/
public function delete( UUID $descendant ) {
$dbw = $this->dbFactory->getDB( DB_MASTER );
$res = $dbw->delete(
$this->tableName,
array(
'tree_descendant_id' => $descendant->getBinary(),
),
__METHOD__
);
if ( $res ) {
$subtreeKey = $this->cacheKey( 'subtree', $descendant );
$parentKey = $this->cacheKey( 'parent', $descendant );
$pathKey = $this->cacheKey( 'rootpath', $descendant );
$this->cache->delete( $subtreeKey );
$this->cache->delete( $parentKey );
$this->cache->delete( $pathKey );
}
return $res;
}
public function findParent( UUID $descendant ) {
$map = $this->fetchParentMap( array( $descendant ) );
return isset( $map[$descendant->getAlphadecimal()] ) ? $map[$descendant->getAlphadecimal()] : null;
}
/**
* Given a list of nodes, find the path from each node to the root of its tree.
* the root must be the first element of the array, $node must be the last element.
* @param UUID[] $descendants Array of UUID objects to find the root paths for.
* @return UUID[][] Associative array, key is the post ID in hex, value is the path as an array.
*/
public function findRootPaths( array $descendants ) {
// alphadecimal => cachekey
$cacheKeys = array();
// alphadecimal => cache result ( distance => parent uuid obj )
$cacheValues = array();
// list of binary values for db query
$missingValues = array();
// alphadecimal => distance => parent uuid obj
$paths = array();
foreach( $descendants as $descendant ) {
$cacheKeys[$descendant->getAlphadecimal()] = $this->cacheKey( 'rootpath', $descendant );
}
$cacheResult = $this->cache->getMulti( array_values( $cacheKeys ) );
foreach( $descendants as $descendant ) {
$alpha = $descendant->getAlphadecimal();
if ( isset( $cacheResult[$cacheKeys[$alpha]] ) ) {
$cacheValues[$alpha] = $cacheResult[$cacheKeys[$alpha]];
} else {
$missingValues[] = $descendant->getBinary();
$paths[$alpha] = array();
}
}
if ( ! count( $missingValues ) ) {
return $cacheValues;
}
$dbr = $this->dbFactory->getDB( DB_SLAVE );
$res = $dbr->select(
$this->tableName,
array( 'tree_descendant_id', 'tree_ancestor_id', 'tree_depth' ),
array(
'tree_descendant_id' => $missingValues,
),
__METHOD__
);
if ( !$res || $res->numRows() === 0 ) {
return $cacheValues;
}
foreach ( $res as $row ) {
$alpha = UUID::create( $row->tree_descendant_id )->getAlphadecimal();
$paths[$alpha][$row->tree_depth] = UUID::create( $row->tree_ancestor_id );
}
foreach( $paths as $descendantId => &$path ) {
if ( !$path ) {
$path = null;
continue;
}
// sort by reverse distance, so furthest away
// parent (root) is at position 0.
ksort( $path );
$path = array_reverse( $path );
$this->cache->set( $cacheKeys[$descendantId], $path );
}
return $paths + $cacheValues;
}
/**
* Finds the root path for a single post ID.
* @param UUID $descendant Post ID
* @return UUID[]|null Path to the root of that node.
*/
public function findRootPath( UUID $descendant ) {
$paths = $this->findRootPaths( array( $descendant ) );
return isset( $paths[$descendant->getAlphadecimal()] ) ? $paths[$descendant->getAlphadecimal()] : null;
}
/**
* Finds the root posts of a list of posts.
* @param UUID[] $descendants Array of PostRevision objects to find roots for.
* @return UUID[] Associative array of post ID (as hex) to UUID object representing its root.
*/
public function findRoots( array $descendants ) {
$paths = $this->findRootPaths( $descendants );
$roots = array();
foreach( $descendants as $descendant ) {
$alpha = $descendant->getAlphadecimal();
if ( isset( $paths[$alpha] ) ) {
$roots[$alpha] = $paths[$alpha][0];
}
}
return $roots;
}
/**
* Given a specific child node find the associated root node
*
* @param UUID $descendant
* @return UUID
* @throws DataModelException
*/
public function findRoot( UUID $descendant ) {
// To simplify caching we will work through the root path instead
// of caching our own value
$path = $this->findRootPath( $descendant );
if ( !$path ) {
throw new DataModelException( $descendant->getAlphadecimal().' has no root post. Probably is a root post.', 'process-data' );
}
$root = array_shift( $path );
return $root;
}
/**
* Fetch a node and all its descendants. Children are returned in the
* same order they were inserted.
*
* @param UUID|UUID[] $roots
* @return array Multi-dimensional tree. The top level is a map from the uuid of a node
* to attributes about that node. The top level contains not just the parents, but all nodes
* within this tree. Within each node there is a 'children' key that contains a map from
* the child uuid's to references back to the top level of this identity map. As such this
* result can be read either as a list or a tree.
* @throws DataModelException When invalid data is received from self::fetchSubtreeNodeList
*/
public function fetchSubtreeIdentityMap( $roots ) {
$roots = ObjectManager::makeArray( $roots );
if ( !$roots ) {
return array();
}
$nodes = $this->fetchSubtreeNodeList( $roots );
if ( !$nodes ) {
throw new DataModelException( 'subtree node list should have at least returned root: ' . $root, 'process-data' );
} elseif ( count( $nodes ) === 1 ) {
$parentMap = $this->fetchParentMap( reset( $nodes ) );
} else {
$parentMap = $this->fetchParentMap( call_user_func_array( 'array_merge', $nodes ) );
}
$identityMap = array();
foreach ( $parentMap as $child => $parent ) {
if ( !array_key_exists( $child, $identityMap ) ) {
$identityMap[$child] = array( 'children' => array() );
}
// Root nodes have no parent
if ( $parent !== null ) {
$identityMap[$parent->getAlphadecimal()]['children'][$child] =& $identityMap[$child];
}
}
foreach ( array_keys( $identityMap ) as $parent ) {
ksort( $identityMap[$parent]['children'] );
}
return $identityMap;
}
public function fetchSubtree( UUID $root, $maxDepth = null ) {
$identityMap = $this->fetchSubtreeIdentityMap( $root, $maxDepth );
if ( !isset( $identityMap[$root->getAlphadecimal()] ) ) {
throw new DataModelException( 'No root exists in the identityMap', 'process-data' );
}
return $identityMap[$root->getAlphadecimal()];
}
public function fetchFullTree( UUID $nodeId ) {
return $this->fetchSubtree( $this->findRoot( $nodeId ) );
}
/**
* Return the id's of all nodes which are a descendant of provided roots
*
* @param UUID[] $roots
* @return array|bool map from root id to its descendant list or false
* @throws \Flow\Exception\InvalidInputException
*/
public function fetchSubtreeNodeList( array $roots ) {
$list = new MultiGetList( $this->cache );
$res = $list->get(
'subtree',
$roots,
array( $this, 'fetchSubtreeNodeListFromDb' )
);
if ( $res === false ) {
wfDebugLog( 'Flow', __METHOD__ . ': Failure fetching node list from cache' );
return false;
}
// $idx is a binary UUID
$retval = array();
foreach ( $res as $idx => $val ) {
$retval[UUID::create( $idx )->getAlphadecimal()] = $val;
}
return $retval;
}
public function fetchSubtreeNodeListFromDb( array $roots ) {
$res = $this->dbFactory->getDB( DB_SLAVE )->select(
$this->tableName,
array( 'tree_ancestor_id', 'tree_descendant_id' ),
array(
'tree_ancestor_id' => UUID::convertUUIDs( $roots ),
),
__METHOD__
);
if ( $res === false ) {
wfDebugLog( 'Flow', __METHOD__ . ': Failure fetching node list from database' );
return false;
}
if ( !$res ) {
return array();
}
$nodes = array();
foreach ( $res as $node ) {
$ancestor = UUID::create( $node->tree_ancestor_id );
$descendant = UUID::create( $node->tree_descendant_id );
$nodes[$ancestor->getAlphadecimal()][$descendant->getAlphadecimal()] = $descendant;
}
return $nodes;
}
/**
* Fetch the id of the immediate parent node of all ids in $nodes. Non-existent
* nodes are not represented in the result set.
*/
public function fetchParentMap( array $nodes ) {
$list = new MultiGetList( $this->cache );
return $list->get(
'parent',
$nodes,
array( $this, 'fetchParentMapFromDb' )
);
}
/**
* @param UUID[] $nodes
* @return UUID[]
* @throws \Flow\Exception\DataModelException
*/
public function fetchParentMapFromDb( array $nodes ) {
// Find out who the parent is for those nodes
$dbr = $this->dbFactory->getDB( DB_SLAVE );
$res = $dbr->select(
$this->tableName,
array( 'tree_ancestor_id', 'tree_descendant_id' ),
array(
'tree_descendant_id' => UUID::convertUUIDs( $nodes ),
'tree_depth' => 1,
),
__METHOD__
);
if ( !$res ) {
return array();
}
$result = array();
foreach ( $res as $node ) {
if ( isset( $result[$node->tree_descendant_id] ) ) {
throw new DataModelException( 'Already have a parent for ' . $node->tree_descendant_id, 'process-data' );
}
$descendant = UUID::create( $node->tree_descendant_id );
$result[$descendant->getAlphadecimal()] = UUID::create( $node->tree_ancestor_id );
}
foreach ( $nodes as $node ) {
if ( !isset( $result[$node->getAlphadecimal()] ) ) {
// $node is a root, it has no parent
$result[$node->getAlphadecimal()] = null;
}
}
return $result;
}
}