@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow edges to be configured to prevent cycles

Summary:
Certain types of things we should be storing in edges (notably, Task X depends on Task Y) should always be acyclic. Allow `PhabricatorEdgeEditor` to enforce this, since we can't correctly enforce it outside of the editor without being vulnerable to races.

Each edge type can be marked acyclic. If an edge type is acyclic, we perform additional steps when writing new edges of that type:

- We acquire a global lock on the edge type before performing any reads or writes. This ensures we can't produce a cycle as a result of a race where two edits add edges which independently do not produce a cycle, but do produce a cycle when combined.
- After performing writes but before committing transactions, we load the edge graph for each acyclic type and verify that it is, in fact, acyclic. If we detect cycles, we abort the edit.
- When we're done, we release the edge type locks.

This is a relatively high-complexity change, but gives us a simple way to flag an edge type as acyclic in a robust way.

Test Plan: Ran unit tests.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1162

Differential Revision: https://secure.phabricator.com/D2940

+298 -12
+6
src/__phutil_library_map__.php
··· 621 621 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', 622 622 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 623 623 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 624 + 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', 624 625 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 626 + 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 625 627 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', 628 + 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 626 629 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', 627 630 'PhabricatorEmailTokenController' => 'applications/auth/controller/PhabricatorEmailTokenController.php', 628 631 'PhabricatorEmailVerificationController' => 'applications/people/controller/PhabricatorEmailVerificationController.php', ··· 1643 1646 'PhabricatorDraft' => 'PhabricatorDraftDAO', 1644 1647 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 1645 1648 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 1649 + 'PhabricatorEdgeCycleException' => 'Exception', 1650 + 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 1646 1651 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 1652 + 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 1647 1653 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 1648 1654 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 1649 1655 'PhabricatorEmailVerificationController' => 'PhabricatorPeopleController',
+81
src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorEdgeTestCase extends PhabricatorTestCase { 20 + 21 + protected function getPhabricatorTestCaseConfiguration() { 22 + return array( 23 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 24 + ); 25 + } 26 + 27 + public function testCycleDetection() { 28 + 29 + // The editor should detect that this introduces a cycle and prevent the 30 + // edit. 31 + 32 + $user = new PhabricatorUser(); 33 + 34 + $obj1 = id(new HarbormasterObject())->save(); 35 + $obj2 = id(new HarbormasterObject())->save(); 36 + $phid1 = $obj1->getPHID(); 37 + $phid2 = $obj2->getPHID(); 38 + 39 + $editor = id(new PhabricatorEdgeEditor()) 40 + ->setUser($user) 41 + ->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2) 42 + ->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1); 43 + 44 + $caught = null; 45 + try { 46 + $editor->save(); 47 + } catch (Exception $ex) { 48 + $caught = $ex; 49 + } 50 + 51 + $this->assertEqual( 52 + true, 53 + $caught instanceof Exception); 54 + 55 + 56 + // The first edit should go through (no cycle), bu the second one should 57 + // fail (it introduces a cycle). 58 + 59 + $editor = id(new PhabricatorEdgeEditor()) 60 + ->setUser($user) 61 + ->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2) 62 + ->save(); 63 + 64 + $editor = id(new PhabricatorEdgeEditor()) 65 + ->setUser($user) 66 + ->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1); 67 + 68 + $caught = null; 69 + try { 70 + $editor->save(); 71 + } catch (Exception $ex) { 72 + $caught = $ex; 73 + } 74 + 75 + $this->assertEqual( 76 + true, 77 + $caught instanceof Exception); 78 + } 79 + 80 + 81 + }
+10
src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
··· 24 24 const TYPE_TASK_HAS_COMMIT = 1; 25 25 const TYPE_COMMIT_HAS_TASK = 2; 26 26 27 + const TYPE_TEST_NO_CYCLE = 9000; 28 + 27 29 public static function getInverse($edge_type) { 28 30 static $map = array( 29 31 self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK, ··· 33 35 return idx($map, $edge_type); 34 36 } 35 37 38 + public static function shouldPreventCycles($edge_type) { 39 + static $map = array( 40 + self::TYPE_TEST_NO_CYCLE => true, 41 + ); 42 + return isset($map[$edge_type]); 43 + } 44 + 36 45 public static function establishConnection($phid_type, $conn_type) { 37 46 static $class_map = array( 38 47 PhabricatorPHIDConstants::PHID_TYPE_TASK => 'ManiphestTask', ··· 43 52 PhabricatorPHIDConstants::PHID_TYPE_PROJ => 'PhabricatorProject', 44 53 PhabricatorPHIDConstants::PHID_TYPE_MLST => 45 54 'PhabricatorMetaMTAMailingList', 55 + PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject', 46 56 ); 47 57 48 58 $class = idx($class_map, $phid_type);
+109 -12
src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
··· 32 32 * ->save(); 33 33 * 34 34 * @task edit Editing Edges 35 + * @task cycles Cycle Prevention 35 36 * @task internal Internals 36 37 */ 37 38 final class PhabricatorEdgeEditor { ··· 113 114 */ 114 115 public function save() { 115 116 116 - // NOTE: We write edge data first, before doing any transactions, since 117 - // it's OK if we just leave it hanging out in space unattached to anything. 117 + $cycle_types = $this->getPreventCyclesEdgeTypes(); 118 + 119 + $locks = array(); 120 + $caught = null; 121 + try { 122 + 123 + // NOTE: We write edge data first, before doing any transactions, since 124 + // it's OK if we just leave it hanging out in space unattached to 125 + // anything. 126 + $this->writeEdgeData(); 127 + 128 + // If we're going to perform cycle detection, lock the edge type before 129 + // doing edits. 130 + if ($cycle_types) { 131 + $src_phids = ipull($this->addEdges, 'src'); 132 + foreach ($cycle_types as $cycle_type) { 133 + $key = 'edge.cycle:'.$cycle_type; 134 + $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15); 135 + } 136 + } 137 + 138 + static $id = 0; 139 + $id++; 140 + 141 + $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); 142 + 143 + // NOTE: Removes first, then adds, so that "remove + add" is a useful 144 + // operation meaning "overwrite". 118 145 119 - $this->writeEdgeData(); 146 + $this->executeRemoves(); 147 + $this->executeAdds(); 120 148 121 - static $id = 0; 122 - $id++; 149 + foreach ($cycle_types as $cycle_type) { 150 + $this->detectCycles($src_phids, $cycle_type); 151 + } 123 152 124 - $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); 153 + $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); 125 154 126 - // NOTE: Removes first, then adds, so that "remove + add" is a useful 127 - // operation meaning "overwrite". 155 + $this->saveTransactions(); 156 + } catch (Exception $ex) { 157 + $caught = $ex; 158 + } 128 159 129 - $this->executeRemoves(); 130 - $this->executeAdds(); 131 160 132 - $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); 161 + if ($caught) { 162 + $this->killTransactions(); 163 + } 133 164 165 + foreach ($locks as $lock) { 166 + $lock->unlock(); 167 + } 134 168 135 - $this->saveTransactions(); 169 + if ($caught) { 170 + throw $caught; 171 + } 136 172 } 137 173 138 174 ··· 327 363 } 328 364 } 329 365 366 + private function killTransactions() { 367 + foreach ($this->openTransactions as $key => $conn_w) { 368 + $conn_w->killTransaction(); 369 + unset($this->openTransactions[$key]); 370 + } 371 + } 372 + 330 373 private function sendEvent($edit_id, $event_type) { 331 374 $event = new PhabricatorEvent( 332 375 $event_type, ··· 337 380 )); 338 381 $event->setUser($this->user); 339 382 PhutilEventEngine::dispatchEvent($event); 383 + } 384 + 385 + 386 + /* -( Cycle Prevention )--------------------------------------------------- */ 387 + 388 + 389 + /** 390 + * Get a list of all edge types which are being added, and which we should 391 + * prevent cycles on. 392 + * 393 + * @return list<const> List of edge types which should have cycles prevented. 394 + * @task cycle 395 + */ 396 + private function getPreventCyclesEdgeTypes() { 397 + $edge_types = array(); 398 + foreach ($this->addEdges as $edge) { 399 + $edge_types[$edge['type']] = true; 400 + } 401 + foreach ($edge_types as $type => $ignored) { 402 + if (!PhabricatorEdgeConfig::shouldPreventCycles($type)) { 403 + unset($edge_types[$type]); 404 + } 405 + } 406 + return array_keys($edge_types); 407 + } 408 + 409 + 410 + /** 411 + * Detect graph cycles of a given edge type. If the edit introduces a cycle, 412 + * a @{class:PhabricatorEdgeCycleException} is thrown with details. 413 + * 414 + * @return void 415 + * @task cycle 416 + */ 417 + private function detectCycles(array $phids, $edge_type) { 418 + // For simplicity, we just seed the graph with the affected nodes rather 419 + // than seeding it with their edges. To do this, we just add synthetic 420 + // edges from an imaginary '<seed>' node to the known edges. 421 + 422 + 423 + $graph = id(new PhabricatorEdgeGraph()) 424 + ->setEdgeType($edge_type) 425 + ->addNodes( 426 + array( 427 + '<seed>' => $phids, 428 + )) 429 + ->loadGraph(); 430 + 431 + foreach ($phids as $phid) { 432 + $cycle = $graph->detectCycles($phid); 433 + if ($cycle) { 434 + throw new PhabricatorEdgeCycleException($edge_type, $cycle); 435 + } 436 + } 340 437 } 341 438 342 439 }
+42
src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorEdgeCycleException extends Exception { 20 + 21 + private $cycleEdgeType; 22 + private $cycle; 23 + 24 + public function __construct($cycle_edge_type, array $cycle) { 25 + $this->cycleEdgeType = $cycle_edge_type; 26 + $this->cycle = $cycle; 27 + 28 + $cycle_list = implode(', ', $cycle); 29 + 30 + parent::__construct( 31 + "Graph cycle detected (type={$cycle_edge_type}, cycle={$cycle_list})."); 32 + } 33 + 34 + public function getCycle() { 35 + return $this->cycle; 36 + } 37 + 38 + public function getCycleEdgeType() { 39 + return $this->cycleEdgeType; 40 + } 41 + 42 + }
+50
src/infrastructure/edges/util/PhabricatorEdgeGraph.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorEdgeGraph extends AbstractDirectedGraph { 20 + 21 + private $edgeType; 22 + 23 + public function setEdgeType($edge_type) { 24 + $this->edgeType = $edge_type; 25 + return $this; 26 + } 27 + 28 + protected function loadEdges(array $nodes) { 29 + if (!$this->edgeType) { 30 + throw new Exception("Set edge type before loading graph!"); 31 + } 32 + 33 + $edges = id(new PhabricatorEdgeQuery()) 34 + ->withSourcePHIDs($nodes) 35 + ->withEdgeTypes(array($this->edgeType)) 36 + ->execute(); 37 + 38 + $results = array_fill_keys($nodes, array()); 39 + foreach ($edges as $src => $types) { 40 + foreach ($types as $type => $dsts) { 41 + foreach ($dsts as $dst => $edge) { 42 + $results[$src][] = $dst; 43 + } 44 + } 45 + } 46 + 47 + return $results; 48 + } 49 + 50 + }