Page MenuHomec4science

PhabricatorEdgeEditor.php
No OneTemporary

File Metadata

Created
Mon, Oct 14, 12:26

PhabricatorEdgeEditor.php

<?php
/**
* Add and remove edges between objects. You can use
* @{class:PhabricatorEdgeQuery} to load object edges. For more information
* on edges, see @{article:Using Edges}.
*
* Edges are not directly policy aware, and this editor makes low-level changes
* below the policy layer.
*
* name=Adding Edges
* $src = $earth_phid;
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
* $dst = $moon_phid;
*
* id(new PhabricatorEdgeEditor())
* ->addEdge($src, $type, $dst)
* ->save();
*
* @task edit Editing Edges
* @task cycles Cycle Prevention
* @task internal Internals
*/
final class PhabricatorEdgeEditor extends Phobject {
private $addEdges = array();
private $remEdges = array();
private $openTransactions = array();
/* -( 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++;
// 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->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,
);
$type_obj = PhabricatorEdgeType::getByConstant($type);
$inverse = $type_obj->getInverseEdgeConstant();
if ($inverse !== null) {
// 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,
'(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 (%Q)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
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]);
}
}
/* -( 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) {
$type_obj = PhabricatorEdgeType::getByConstant($type);
if (!$type_obj->shouldPreventCycles()) {
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);
}
}
}
}

Event Timeline