diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php index aa3400a3a..2287ca7ed 100644 --- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php +++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php @@ -1,460 +1,460 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Add and remove edges between objects. You can use * @{class:PhabricatorEdgeQuery} to load object edges. For more information * on edges, see @{article:Using Edges}. * * name=Adding Edges * $src = $earth_phid; * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE; * $dst = $moon_phid; * * id(new PhabricatorEdgeEditor()) * ->addEdge($src, $type, $dst) * ->setUser($user) * ->save(); * * @task edit Editing Edges * @task cycles Cycle Prevention * @task internal Internals */ final class PhabricatorEdgeEditor { private $addEdges = array(); private $remEdges = array(); private $openTransactions = array(); private $user; private $suppressEvents; public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } /* -( 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 + * you call @{method:save}. If the edge already exists, it will not be * overwritten. 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 + * 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 IGNORE INTO %T (src, type, dst, dateCreated, seq, dataID) VALUES %Q', 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)', $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)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(', ', $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->user); 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<const> 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 '<seed>' node to the known edges. $graph = id(new PhabricatorEdgeGraph()) ->setEdgeType($edge_type) ->addNodes( array( '<seed>' => $phids, )) ->loadGraph(); foreach ($phids as $phid) { $cycle = $graph->detectCycles($phid); if ($cycle) { throw new PhabricatorEdgeCycleException($edge_type, $cycle); } } } }