Path: blob/master/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
13419 views
<?php12/**3* Add and remove edges between objects. You can use4* @{class:PhabricatorEdgeQuery} to load object edges. For more information5* on edges, see @{article:Using Edges}.6*7* Edges are not directly policy aware, and this editor makes low-level changes8* below the policy layer.9*10* name=Adding Edges11* $src = $earth_phid;12* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;13* $dst = $moon_phid;14*15* id(new PhabricatorEdgeEditor())16* ->addEdge($src, $type, $dst)17* ->save();18*19* @task edit Editing Edges20* @task cycles Cycle Prevention21* @task internal Internals22*/23final class PhabricatorEdgeEditor extends Phobject {2425private $addEdges = array();26private $remEdges = array();27private $openTransactions = array();282930/* -( Editing Edges )------------------------------------------------------ */313233/**34* Add a new edge (possibly also adding its inverse). Changes take effect when35* you call @{method:save}. If the edge already exists, it will not be36* overwritten, but if data is attached to the edge it will be updated.37* Removals queued with @{method:removeEdge} are executed before38* adds, so the effect of removing and adding the same edge is to overwrite39* any existing edge.40*41* The `$options` parameter accepts these values:42*43* - `data` Optional, data to write onto the edge.44* - `inverse_data` Optional, data to write on the inverse edge. If not45* provided, `data` will be written.46*47* @param phid Source object PHID.48* @param const Edge type constant.49* @param phid Destination object PHID.50* @param map Options map (see documentation).51* @return this52*53* @task edit54*/55public function addEdge($src, $type, $dst, array $options = array()) {56foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {57$this->addEdges[] = $spec;58}59return $this;60}616263/**64* Remove an edge (possibly also removing its inverse). Changes take effect65* when you call @{method:save}. If an edge does not exist, the removal66* will be ignored. Edges are added after edges are removed, so the effect of67* a remove plus an add is to overwrite.68*69* @param phid Source object PHID.70* @param const Edge type constant.71* @param phid Destination object PHID.72* @return this73*74* @task edit75*/76public function removeEdge($src, $type, $dst) {77foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {78$this->remEdges[] = $spec;79}80return $this;81}828384/**85* Apply edge additions and removals queued by @{method:addEdge} and86* @{method:removeEdge}. Note that transactions are opened, all additions and87* removals are executed, and then transactions are saved. Thus, in some cases88* it may be slightly more efficient to perform multiple edit operations89* (e.g., adds followed by removals) if their outcomes are not dependent,90* since transactions will not be held open as long.91*92* @return this93* @task edit94*/95public function save() {9697$cycle_types = $this->getPreventCyclesEdgeTypes();9899$locks = array();100$caught = null;101try {102103// NOTE: We write edge data first, before doing any transactions, since104// it's OK if we just leave it hanging out in space unattached to105// anything.106$this->writeEdgeData();107108// If we're going to perform cycle detection, lock the edge type before109// doing edits.110if ($cycle_types) {111$src_phids = ipull($this->addEdges, 'src');112foreach ($cycle_types as $cycle_type) {113$key = 'edge.cycle:'.$cycle_type;114$locks[] = PhabricatorGlobalLock::newLock($key)->lock(15);115}116}117118static $id = 0;119$id++;120121// NOTE: Removes first, then adds, so that "remove + add" is a useful122// operation meaning "overwrite".123124$this->executeRemoves();125$this->executeAdds();126127foreach ($cycle_types as $cycle_type) {128$this->detectCycles($src_phids, $cycle_type);129}130131$this->saveTransactions();132} catch (Exception $ex) {133$caught = $ex;134}135136if ($caught) {137$this->killTransactions();138}139140foreach ($locks as $lock) {141$lock->unlock();142}143144if ($caught) {145throw $caught;146}147}148149150/* -( Internals )---------------------------------------------------------- */151152153/**154* Build the specification for an edge operation, and possibly build its155* inverse as well.156*157* @task internal158*/159private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {160$data = array();161if (!empty($options['data'])) {162$data['data'] = $options['data'];163}164165$src_type = phid_get_type($src);166$dst_type = phid_get_type($dst);167168$specs = array();169$specs[] = array(170'src' => $src,171'src_type' => $src_type,172'dst' => $dst,173'dst_type' => $dst_type,174'type' => $type,175'data' => $data,176);177178$type_obj = PhabricatorEdgeType::getByConstant($type);179$inverse = $type_obj->getInverseEdgeConstant();180if ($inverse !== null) {181182// If `inverse_data` is set, overwrite the edge data. Normally, just183// write the same data to the inverse edge.184if (array_key_exists('inverse_data', $options)) {185$data['data'] = $options['inverse_data'];186}187188$specs[] = array(189'src' => $dst,190'src_type' => $dst_type,191'dst' => $src,192'dst_type' => $src_type,193'type' => $inverse,194'data' => $data,195);196}197198return $specs;199}200201202/**203* Write edge data.204*205* @task internal206*/207private function writeEdgeData() {208$adds = $this->addEdges;209210$writes = array();211foreach ($adds as $key => $edge) {212if ($edge['data']) {213$writes[] = array($key, $edge['src_type'], json_encode($edge['data']));214}215}216217foreach ($writes as $write) {218list($key, $src_type, $data) = $write;219$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');220queryfx(221$conn_w,222'INSERT INTO %T (data) VALUES (%s)',223PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,224$data);225$this->addEdges[$key]['data_id'] = $conn_w->getInsertID();226}227}228229230/**231* Add queued edges.232*233* @task internal234*/235private function executeAdds() {236$adds = $this->addEdges;237$adds = igroup($adds, 'src_type');238239// Assign stable sequence numbers to each edge, so we have a consistent240// ordering across edges by source and type.241foreach ($adds as $src_type => $edges) {242$edges_by_src = igroup($edges, 'src');243foreach ($edges_by_src as $src => $src_edges) {244$seq = 0;245foreach ($src_edges as $key => $edge) {246$src_edges[$key]['seq'] = $seq++;247$src_edges[$key]['dateCreated'] = time();248}249$edges_by_src[$src] = $src_edges;250}251$adds[$src_type] = array_mergev($edges_by_src);252}253254$inserts = array();255foreach ($adds as $src_type => $edges) {256$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');257$sql = array();258foreach ($edges as $edge) {259$sql[] = qsprintf(260$conn_w,261'(%s, %d, %s, %d, %d, %nd)',262$edge['src'],263$edge['type'],264$edge['dst'],265$edge['dateCreated'],266$edge['seq'],267idx($edge, 'data_id'));268}269$inserts[] = array($conn_w, $sql);270}271272foreach ($inserts as $insert) {273list($conn_w, $sql) = $insert;274$conn_w->openTransaction();275$this->openTransactions[] = $conn_w;276277foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {278queryfx(279$conn_w,280'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID)281VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)',282PhabricatorEdgeConfig::TABLE_NAME_EDGE,283$chunk);284}285}286}287288289/**290* Remove queued edges.291*292* @task internal293*/294private function executeRemoves() {295$rems = $this->remEdges;296$rems = igroup($rems, 'src_type');297298$deletes = array();299foreach ($rems as $src_type => $edges) {300$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');301$sql = array();302foreach ($edges as $edge) {303$sql[] = qsprintf(304$conn_w,305'(src = %s AND type = %d AND dst = %s)',306$edge['src'],307$edge['type'],308$edge['dst']);309}310$deletes[] = array($conn_w, $sql);311}312313foreach ($deletes as $delete) {314list($conn_w, $sql) = $delete;315316$conn_w->openTransaction();317$this->openTransactions[] = $conn_w;318319foreach (array_chunk($sql, 256) as $chunk) {320queryfx(321$conn_w,322'DELETE FROM %T WHERE %LO',323PhabricatorEdgeConfig::TABLE_NAME_EDGE,324$chunk);325}326}327}328329330/**331* Save open transactions.332*333* @task internal334*/335private function saveTransactions() {336foreach ($this->openTransactions as $key => $conn_w) {337$conn_w->saveTransaction();338unset($this->openTransactions[$key]);339}340}341342private function killTransactions() {343foreach ($this->openTransactions as $key => $conn_w) {344$conn_w->killTransaction();345unset($this->openTransactions[$key]);346}347}348349350/* -( Cycle Prevention )--------------------------------------------------- */351352353/**354* Get a list of all edge types which are being added, and which we should355* prevent cycles on.356*357* @return list<const> List of edge types which should have cycles prevented.358* @task cycle359*/360private function getPreventCyclesEdgeTypes() {361$edge_types = array();362foreach ($this->addEdges as $edge) {363$edge_types[$edge['type']] = true;364}365foreach ($edge_types as $type => $ignored) {366$type_obj = PhabricatorEdgeType::getByConstant($type);367if (!$type_obj->shouldPreventCycles()) {368unset($edge_types[$type]);369}370}371return array_keys($edge_types);372}373374375/**376* Detect graph cycles of a given edge type. If the edit introduces a cycle,377* a @{class:PhabricatorEdgeCycleException} is thrown with details.378*379* @return void380* @task cycle381*/382private function detectCycles(array $phids, $edge_type) {383// For simplicity, we just seed the graph with the affected nodes rather384// than seeding it with their edges. To do this, we just add synthetic385// edges from an imaginary '<seed>' node to the known edges.386387388$graph = id(new PhabricatorEdgeGraph())389->setEdgeType($edge_type)390->addNodes(391array(392'<seed>' => $phids,393))394->loadGraph();395396foreach ($phids as $phid) {397$cycle = $graph->detectCycles($phid);398if ($cycle) {399throw new PhabricatorEdgeCycleException($edge_type, $cycle);400}401}402}403404405}406407408