diff --git a/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php index 4e1467e6f7..074aaf92d0 100644 --- a/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php +++ b/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php @@ -1,188 +1,190 @@ getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit = $drequest->getCommit(); $subpath = $repository->getDetail('svn-subpath'); if ($subpath && strncmp($subpath, $path, strlen($subpath))) { // If we have a subpath and the path isn't a child of it, it (almost // certainly) won't exist since we don't track commits which affect // it. (Even if it exists, return a consistent result.) $this->reason = self::REASON_IS_UNTRACKED_PARENT; return array(); } $conn_r = $repository->establishConnection('r'); $parent_path = DiffusionPathIDQuery::getParentPath($path); $path_query = new DiffusionPathIDQuery( array( $path, $parent_path, )); $path_map = $path_query->loadPathIDs(); $path_id = $path_map[$path]; $parent_path_id = $path_map[$parent_path]; if (empty($path_id)) { $this->reason = self::REASON_IS_NONEXISTENT; return array(); } if ($commit) { $slice_clause = 'AND svnCommit <= '.(int)$commit; } else { $slice_clause = ''; } $index = queryfx_all( $conn_r, 'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE repositoryID = %d AND parentID = %d %Q GROUP BY pathID', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $path_id, $slice_clause); if (!$index) { if ($path == '/') { $this->reason = self::REASON_IS_EMPTY; } else { // NOTE: The parent path ID is included so this query can take // advantage of the table's primary key; it is uniquely determined by // the pathID but if we don't do the lookup ourselves MySQL doesn't have // the information it needs to avoid a table scan. $reasons = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE repositoryID = %d AND parentID = %d AND pathID = %d %Q ORDER BY svnCommit DESC LIMIT 2', PhabricatorRepository::TABLE_FILESYSTEM, $repository->getID(), $parent_path_id, $path_id, $slice_clause); $reason = reset($reasons); if (!$reason) { $this->reason = self::REASON_IS_NONEXISTENT; } else { $file_type = $reason['fileType']; if (empty($reason['existed'])) { $this->reason = self::REASON_IS_DELETED; $this->deletedAtCommit = $reason['svnCommit']; if (!empty($reasons[1])) { $this->existedAtCommit = $reasons[1]['svnCommit']; } } else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { $this->reason = self::REASON_IS_EMPTY; } else { $this->reason = self::REASON_IS_FILE; } } } return array(); } if ($this->shouldOnlyTestValidity()) { return true; } $sql = array(); foreach ($index as $row) { - $sql[] = '('.(int)$row['pathID'].', '.(int)$row['maxCommit'].')'; + $sql[] = + '(pathID = '.(int)$row['pathID'].' AND '. + 'svnCommit = '.(int)$row['maxCommit'].')'; } $browse = queryfx_all( $conn_r, 'SELECT *, p.path pathName FROM %T f JOIN %T p ON f.pathID = p.id WHERE repositoryID = %d AND parentID = %d AND existed = 1 - AND (pathID, svnCommit) in (%Q) + AND (%Q) ORDER BY pathName', PhabricatorRepository::TABLE_FILESYSTEM, PhabricatorRepository::TABLE_PATH, $repository->getID(), $path_id, - implode(', ', $sql)); + implode(' OR ', $sql)); $loadable_commits = array(); foreach ($browse as $key => $file) { // We need to strip out directories because we don't store last-modified // in the filesystem table. if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) { $loadable_commits[] = $file['svnCommit']; $browse[$key]['hasCommit'] = true; } } $commits = array(); $commit_data = array(); if ($loadable_commits) { // NOTE: Even though these are integers, use '%Ls' because MySQL doesn't // use the second part of the key otherwise! $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), $loadable_commits); $commits = mpull($commits, null, 'getCommitIdentifier'); if ($commits) { $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); } else { $commit_data = array(); } } $path_normal = DiffusionPathIDQuery::normalizePath($path); $results = array(); foreach ($browse as $file) { $full_path = $file['pathName']; $file_path = ltrim(substr($full_path, strlen($path_normal)), '/'); $full_path = ltrim($full_path, '/'); $result = new DiffusionRepositoryPath(); $result->setPath($file_path); $result->setFullPath($full_path); // $result->setHash($hash); $result->setFileType($file['fileType']); // $result->setFileSize($size); if (!empty($file['hasCommit'])) { $commit = idx($commits, $file['svnCommit']); if ($commit) { $data = idx($commit_data, $commit->getID()); $result->setLastModifiedCommit($commit); $result->setLastCommitData($data); } } $results[] = $result; } if (empty($results)) { $this->reason = self::REASON_IS_EMPTY; } return $results; } } diff --git a/src/docs/developer/database.diviner b/src/docs/developer/database.diviner index 8ad4e349f3..6b085039cc 100644 --- a/src/docs/developer/database.diviner +++ b/src/docs/developer/database.diviner @@ -1,171 +1,175 @@ @title Database Schema @group developer This document describes key components of the database schema and should answer questions like how to store new types of data. = Database System = Phabricator uses MySQL with InnoDB engine. The only exception is the `search_documentfield` table which uses MyISAM because MySQL doesn't support fulltext search in InnoDB. Let us know if you need to use other database system: @{article:Give Feedback! Get Support!}. = PHP Drivers = Phabricator supports [[ http://www.php.net/book.mysql | MySQL ]] and [[ http://www.php.net/book.mysqli | MySQLi ]] PHP extensions. Most installations use MySQL but MySQLi should work equally well. = Databases = Each Phabricator application has its own database. The names are prefixed by `phabricator_`. This design has two advantages: * Each database is easier to comprehend and to maintain. * We don't do cross-database joins so each database can live on its own machine which is useful for load-balancing. = Connections = Phabricator specifies if it will use any opened connection just for reading or also for writing. This allows opening write connections to master and read connections to slave in master/slave replication. It is useful for load-balancing. = Tables = Each table name is prefixed by its application. For example, Differential revisions are stored in database `phabricator_differential` and table `differential_revision`. This duplicity allows easy recognition of the table in DarkConsole (see @{article:Using DarkConsole}) and other places. The exception is tables which share the same schema over different databases such as `edge`. We use lower-case table names with words separated by underscores. The reason is that MySQL can be configured (with `lower_case_table_names`) to lower-case the table names anyway. = Column Names = Phabricator uses camelCase names for columns. The main advantage is that they directly map to properties in PHP classes. Don't use MySQL reserved words (such as `order`) for column names. = Data Types = Phabricator uses `int unsigned` columns for storing dates instead of `date` or `datetime`. We don't need to care about time-zones in both MySQL and PHP because of it. The other reason is that PHP internally uses numbers for storing dates. Phabricator uses UTF-8 encoding for storing all text data. We use `utf8_general_ci` collation for free-text and `utf8_bin` for identifiers. We don't use the `enum` data type because each change to the list of possible values requires altering the table (which is slow with big tables). We use numbers (or short strings in some cases) mapped to PHP constants instead. = JSON = Some data don't require structured access - you don't need to filter or order by them. We store these data as text fields in JSON format. This approach has several advantages: * If we decide to add another unstructured field then we don't need to alter the table (which is slow for big tables in MySQL). * Table structure is not cluttered by fields which could be unused most of the time. An example of such usage can be found in column `differential_diffproperty.data`. = Primary Keys = Most tables have auto-increment column named `id`. However creating such column is not required for tables which are not usually directly referenced (such as tables expressing M:N relations). Example of such table is `differential_relationship`. = Indexes = Create all indexes necessary for fast query execution in most cases. Don't create indexes which are not used. You can analyze queries @{article:Using DarkConsole}. +Older MySQL versions are not able to use indexes for tuple search: +`(a, b) IN ((%s, %d), (%s, %d))`. Use `AND` and `OR` instead: +`((a = %s AND b = %d) OR (a = %s AND b = %d))`. + = Foreign Keys = We don't use InnoDB's foreign keys because our application is so great that no inconsistencies can arise. It will just slow us down. = PHIDs = Each globally referencable object in Phabricator has its associated PHID (Phabricator ID) which serves as a global identifier. We use PHIDs for referencing data in different databases. We use both autoincrementing IDs and global PHIDs because each is useful in different contexts. Autoincrementing IDs are chronologically ordered and allow us to construct short, human-readable object names (like D2258) and URIs. Global PHIDs allow us to represent relationships between different types of objects in a homogenous way. For example, the concept of "subscribers" is more powerfully done with PHIDs because we could theoretically have users, projects, teams, and more all as "subscribers" of other objects. Using an ID column we would need to add a "type" column to avoid ID collision; using PHIDs does not require this additional column. = Transactions = Transactional code should be written using transactions. Example of such code is inserting multiple records where one doesn't make sense without the other or selecting data later used for update. See chapter in @{class:LiskDAO}. = Advanced Features = We don't use MySQL advanced features such as triggers, stored procedures or events because we like expressing the application logic in PHP more than in SQL. Some of these features (especially triggers) can also cause big confusion. Avoiding these advanced features is also good for supporting other database systems (which we don't support anyway). = Schema Denormalization = Phabricator uses schema denormalization for performance reasons sparingly. Try to avoid it if possible. = Changing the Schema = There are three simple steps to update the schema: # Create a `.sql` file in `resources/sql/patches/`. This file should: - Contain the approprate MySQL commands to update the schema. - Be named as `YYYYMMDD.patchname.ext`. For example, `20130217.example.sql`. - Use `${NAMESPACE}` rather than `phabricator` for database names. - Use `COLLATE utf8_bin` for any columns that are to be used as identifiers, such as PHID columns. Otherwise, use `COLLATE utf8_general_ci`. - Name all indexes so it is possible to delete them later. # Edit `src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php` and add your patch to @{method@phabricator:PhabricatorBuiltinPatchList::getPatches}. # Run `bin/storage upgrade`. It is also possible to create more complex patches in PHP for data migration (due to schema changes or otherwise.) However, the schema changes themselves should be done in separate `.sql` files. Order can be guaranteed by editing `src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php` appropriately. See the [[https://secure.phabricator.com/rPb39175342dc5bee0c2246b05fa277e76a7e96ed3 | commit adding policy storage for Paste ]] for a reasonable example of the code changes. = See Also = * @{class:LiskDAO} * @{class:PhabricatorPHID} diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php index d4996e3e35..e54fb72d3e 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php @@ -1,212 +1,212 @@ ids = $ids; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function execute() { if (!$this->limit) { throw new Exception("You must setLimit() when leasing tasks."); } $task_table = new PhabricatorWorkerActiveTask(); $taskdata_table = new PhabricatorWorkerTaskData(); $lease_ownership_name = $this->getLeaseOwnershipName(); $conn_w = $task_table->establishConnection('w'); // Try to satisfy the request from new, unleased tasks first. If we don't // find enough tasks, try tasks with expired leases (i.e., tasks which have // previously failed). $phases = array( self::PHASE_UNLEASED, self::PHASE_EXPIRED, ); $limit = $this->limit; $leased = 0; foreach ($phases as $phase) { // NOTE: If we issue `UPDATE ... WHERE ... ORDER BY id ASC`, the query // goes very, very slowly. The `ORDER BY` triggers this, although we get // the same apparent results without it. Without the ORDER BY, binary // read slaves complain that the query isn't repeatable. To avoid both // problems, do a SELECT and then an UPDATE. $rows = queryfx_all( $conn_w, 'SELECT id, leaseOwner FROM %T %Q %Q %Q', $task_table->getTableName(), $this->buildWhereClause($conn_w, $phase), $this->buildOrderClause($conn_w), $this->buildLimitClause($conn_w, $limit - $leased)); // NOTE: Sometimes, we'll race with another worker and they'll grab // this task before we do. We could reduce how often this happens by // selecting more tasks than we need, then shuffling them and trying // to lock only the number we're actually after. However, the amount // of time workers spend here should be very small relative to their // total runtime, so keep it simple for the moment. if ($rows) { queryfx( $conn_w, 'UPDATE %T task SET leaseOwner = %s, leaseExpires = UNIX_TIMESTAMP() + %d %Q', $task_table->getTableName(), $lease_ownership_name, self::DEFAULT_LEASE_DURATION, $this->buildUpdateWhereClause($conn_w, $phase, $rows)); $leased += $conn_w->getAffectedRows(); if ($leased == $limit) { break; } } } if (!$leased) { return array(); } $data = queryfx_all( $conn_w, 'SELECT task.*, taskdata.data _taskData, UNIX_TIMESTAMP() _serverTime FROM %T task LEFT JOIN %T taskdata ON taskdata.id = task.dataID WHERE leaseOwner = %s AND leaseExpires > UNIX_TIMESTAMP() %Q %Q', $task_table->getTableName(), $taskdata_table->getTableName(), $lease_ownership_name, $this->buildOrderClause($conn_w), $this->buildLimitClause($conn_w, $limit)); $tasks = $task_table->loadAllFromArray($data); $tasks = mpull($tasks, null, 'getID'); foreach ($data as $row) { $tasks[$row['id']]->setServerTime($row['_serverTime']); if ($row['_taskData']) { $task_data = json_decode($row['_taskData'], true); } else { $task_data = null; } $tasks[$row['id']]->setData($task_data); } return $tasks; } private function buildWhereClause(AphrontDatabaseConnection $conn_w, $phase) { $where = array(); switch ($phase) { case self::PHASE_UNLEASED: $where[] = 'leaseOwner IS NULL'; break; case self::PHASE_EXPIRED: $where[] = 'leaseExpires < UNIX_TIMESTAMP()'; break; default: throw new Exception("Unknown phase '{$phase}'!"); } if ($this->ids) { $where[] = qsprintf( $conn_w, 'task.id IN (%Ld)', $this->ids); } return $this->formatWhereClause($where); } private function buildUpdateWhereClause( AphrontDatabaseConnection $conn_w, $phase, array $rows) { $where = array(); // NOTE: This is basically working around the MySQL behavior that // `IN (NULL)` doesn't match NULL. switch ($phase) { case self::PHASE_UNLEASED: $where[] = qsprintf( $conn_w, 'leaseOwner IS NULL'); $where[] = qsprintf( $conn_w, 'id IN (%Ld)', ipull($rows, 'id')); break; case self::PHASE_EXPIRED: $in = array(); foreach ($rows as $row) { $in[] = qsprintf( $conn_w, - '(%d, %s)', + '(id = %d AND leaseOwner = %s)', $row['id'], $row['leaseOwner']); } $where[] = qsprintf( $conn_w, - '(id, leaseOwner) IN (%Q)', - '('.implode(', ', $in).')'); + '(%Q)', + implode(' OR ', $in)); break; default: throw new Exception("Unknown phase '{$phase}'!"); } return $this->formatWhereClause($where); } private function buildOrderClause(AphrontDatabaseConnection $conn_w) { return qsprintf($conn_w, 'ORDER BY id ASC'); } private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) { return qsprintf($conn_w, 'LIMIT %d', $limit); } private function getLeaseOwnershipName() { static $sequence = 0; $parts = array( getmypid(), time(), php_uname('n'), ++$sequence, ); return implode(':', $parts); } } diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php index edda35cf0f..4ee2b68b84 100644 --- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php +++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php @@ -1,439 +1,439 @@ addEdge($src, $type, $dst) * ->setActor($user) * ->save(); * * @task edit Editing Edges * @task cycles Cycle Prevention * @task internal Internals */ final class PhabricatorEdgeEditor extends PhabricatorEditor { private $addEdges = array(); private $remEdges = array(); private $openTransactions = array(); private $suppressEvents; /* -( Editing Edges )------------------------------------------------------ */ /** * Add a new edge (possibly also adding its inverse). Changes take effect when * you call @{method:save}. If the edge already exists, it will not be * overwritten, but if data is attached to the edge it will be updated. * Removals queued with @{method:removeEdge} are executed before * adds, so the effect of removing and adding the same edge is to overwrite * any existing edge. * * The `$options` parameter accepts these values: * * - `data` Optional, data to write onto the edge. * - `inverse_data` Optional, data to write on the inverse edge. If not * provided, `data` will be written. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @param map Options map (see documentation). * @return this * * @task edit */ public function addEdge($src, $type, $dst, array $options = array()) { foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) { $this->addEdges[] = $spec; } return $this; } /** * Remove an edge (possibly also removing its inverse). Changes take effect * when you call @{method:save}. If an edge does not exist, the removal * will be ignored. Edges are added after edges are removed, so the effect of * a remove plus an add is to overwrite. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @return this * * @task edit */ public function removeEdge($src, $type, $dst) { foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) { $this->remEdges[] = $spec; } return $this; } /** * Apply edge additions and removals queued by @{method:addEdge} and * @{method:removeEdge}. Note that transactions are opened, all additions and * removals are executed, and then transactions are saved. Thus, in some cases * it may be slightly more efficient to perform multiple edit operations * (e.g., adds followed by removals) if their outcomes are not dependent, * since transactions will not be held open as long. * * @return this * @task edit */ public function save() { $cycle_types = $this->getPreventCyclesEdgeTypes(); $locks = array(); $caught = null; try { // NOTE: We write edge data first, before doing any transactions, since // it's OK if we just leave it hanging out in space unattached to // anything. $this->writeEdgeData(); // If we're going to perform cycle detection, lock the edge type before // doing edits. if ($cycle_types) { $src_phids = ipull($this->addEdges, 'src'); foreach ($cycle_types as $cycle_type) { $key = 'edge.cycle:'.$cycle_type; $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15); } } static $id = 0; $id++; $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); // NOTE: Removes first, then adds, so that "remove + add" is a useful // operation meaning "overwrite". $this->executeRemoves(); $this->executeAdds(); foreach ($cycle_types as $cycle_type) { $this->detectCycles($src_phids, $cycle_type); } $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); $this->saveTransactions(); } catch (Exception $ex) { $caught = $ex; } if ($caught) { $this->killTransactions(); } foreach ($locks as $lock) { $lock->unlock(); } if ($caught) { throw $caught; } } /* -( Internals )---------------------------------------------------------- */ /** * Build the specification for an edge operation, and possibly build its * inverse as well. * * @task internal */ private function buildEdgeSpecs($src, $type, $dst, array $options = array()) { $data = array(); if (!empty($options['data'])) { $data['data'] = $options['data']; } $src_type = phid_get_type($src); $dst_type = phid_get_type($dst); $specs = array(); $specs[] = array( 'src' => $src, 'src_type' => $src_type, 'dst' => $dst, 'dst_type' => $dst_type, 'type' => $type, 'data' => $data, ); $inverse = PhabricatorEdgeConfig::getInverse($type); if ($inverse) { // If `inverse_data` is set, overwrite the edge data. Normally, just // write the same data to the inverse edge. if (array_key_exists('inverse_data', $options)) { $data['data'] = $options['inverse_data']; } $specs[] = array( 'src' => $dst, 'src_type' => $dst_type, 'dst' => $src, 'dst_type' => $src_type, 'type' => $inverse, 'data' => $data, ); } return $specs; } /** * Write edge data. * * @task internal */ private function writeEdgeData() { $adds = $this->addEdges; $writes = array(); foreach ($adds as $key => $edge) { if ($edge['data']) { $writes[] = array($key, $edge['src_type'], json_encode($edge['data'])); } } foreach ($writes as $write) { list($key, $src_type, $data) = $write; $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); queryfx( $conn_w, 'INSERT INTO %T (data) VALUES (%s)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data); $this->addEdges[$key]['data_id'] = $conn_w->getInsertID(); } } /** * Add queued edges. * * @task internal */ private function executeAdds() { $adds = $this->addEdges; $adds = igroup($adds, 'src_type'); // Assign stable sequence numbers to each edge, so we have a consistent // ordering across edges by source and type. foreach ($adds as $src_type => $edges) { $edges_by_src = igroup($edges, 'src'); foreach ($edges_by_src as $src => $src_edges) { $seq = 0; foreach ($src_edges as $key => $edge) { $src_edges[$key]['seq'] = $seq++; $src_edges[$key]['dateCreated'] = time(); } $edges_by_src[$src] = $src_edges; } $adds[$src_type] = array_mergev($edges_by_src); } $inserts = array(); foreach ($adds as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(%s, %d, %s, %d, %d, %nd)', $edge['src'], $edge['type'], $edge['dst'], $edge['dateCreated'], $edge['seq'], idx($edge, 'data_id')); } $inserts[] = array($conn_w, $sql); } foreach ($inserts as $insert) { list($conn_w, $sql) = $insert; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID) VALUES %Q ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(', ', $chunk)); } } } /** * Remove queued edges. * * @task internal */ private function executeRemoves() { $rems = $this->remEdges; $rems = igroup($rems, 'src_type'); $deletes = array(); foreach ($rems as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, - '(%s, %d, %s)', + '(src = %s AND type = %d AND dst = %s)', $edge['src'], $edge['type'], $edge['dst']); } $deletes[] = array($conn_w, $sql); } foreach ($deletes as $delete) { list($conn_w, $sql) = $delete; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, - 'DELETE FROM %T WHERE (src, type, dst) IN (%Q)', + 'DELETE FROM %T WHERE (%Q)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, - implode(', ', $chunk)); + implode(' OR ', $chunk)); } } } /** * Save open transactions. * * @task internal */ private function saveTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->saveTransaction(); unset($this->openTransactions[$key]); } } private function killTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->killTransaction(); unset($this->openTransactions[$key]); } } /** * Suppress edge edit events. This prevents listeners from making updates in * response to edits, and is primarily useful when performing migrations. You * should not normally need to use it. * * @param bool True to supress events related to edits. * @return this * @task internal */ public function setSuppressEvents($suppress) { $this->suppressEvents = $suppress; return $this; } private function sendEvent($edit_id, $event_type) { if ($this->suppressEvents) { return; } $event = new PhabricatorEvent( $event_type, array( 'id' => $edit_id, 'add' => $this->addEdges, 'rem' => $this->remEdges, )); $event->setUser($this->getActor()); PhutilEventEngine::dispatchEvent($event); } /* -( Cycle Prevention )--------------------------------------------------- */ /** * Get a list of all edge types which are being added, and which we should * prevent cycles on. * * @return list List of edge types which should have cycles prevented. * @task cycle */ private function getPreventCyclesEdgeTypes() { $edge_types = array(); foreach ($this->addEdges as $edge) { $edge_types[$edge['type']] = true; } foreach ($edge_types as $type => $ignored) { if (!PhabricatorEdgeConfig::shouldPreventCycles($type)) { unset($edge_types[$type]); } } return array_keys($edge_types); } /** * Detect graph cycles of a given edge type. If the edit introduces a cycle, * a @{class:PhabricatorEdgeCycleException} is thrown with details. * * @return void * @task cycle */ private function detectCycles(array $phids, $edge_type) { // For simplicity, we just seed the graph with the affected nodes rather // than seeding it with their edges. To do this, we just add synthetic // edges from an imaginary '' node to the known edges. $graph = id(new PhabricatorEdgeGraph()) ->setEdgeType($edge_type) ->addNodes( array( '' => $phids, )) ->loadGraph(); foreach ($phids as $phid) { $cycle = $graph->detectCycles($phid); if ($cycle) { throw new PhabricatorEdgeCycleException($edge_type, $cycle); } } } }