@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.

Apply inverse edge edits after committing primary object edits

Summary:
Fixes T13082. When you create a revision (say, `D111`) with `Ref T222` in the body, we write a `D111 -> T222` edge ("revision 111 references task 222") and an inverse `T222 -> D111` edge ("task 222 is referenced by revision 111").

We also apply a transaction to `D111` ("alice added a task: Txxx.") and an inverse transaction to `T222` ("alice added a revision: Dxxx").

Currently, it appears that the inverse transaction can sometimes generate mail faster than `D111` actually commits its (database) transactions, so the mail says "alice added a revision: Unknown Object (Differential Revision)". See T13082 for evidence that this is true, and a reproduction case.

To fix this, apply the inverse transaction (to `T222`) after we commit the main object (here, `D111`).

This is tricky because when we apply transactions, the transaction editor automatically "fixes" them to be consistent with the database state. For example, if a task already has title "XYZ" and you set the title to "XYZ" (same title), we just no-op the transaction.

It also fixes edge edits. The old sequence was:

- Open (database) transaction.
- Apply our transaction ("alice added a task").
- Apply the inverse transaction ("alice added a revision").
- Write the edges to the database.
- Commit (database) transaction.

Under this sequence, the inverse transaction was "correct" and didn't need to be fixed, so the fixing step didn't touch it.

The new sequence is:

- Open (database) transaction.
- Apply our transaction ("alice added a task").
- Write the edges.
- Commit (database) transaction.
- Apply the inverse transaction ("alice added a revision").

Since the inverse transaction now happens after the database edge write, the fixing step detects that it's a no-op and throws it away if we do this naively.

Instead, add some special cases around inverse edits to skip the correction/fixing logic, and just pass the "right" values in the first place.

Test Plan:
Added and removed related tasks from revisions, saw appropriate transactions render on both objects.

(It's hard to be certain this completely fixes the issue since it only happened occasionally in the first place, but we can see if it happens any more on `secure`.)

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13082, T222

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

+67 -15
+67 -15
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 447 447 'edge:type')); 448 448 } 449 449 450 + // See T13082. If this is an inverse edit, the parent editor has 451 + // already populated the transaction values correctly. 452 + if ($this->getIsInverseEdgeEditor()) { 453 + return $xaction->getOldValue(); 454 + } 455 + 450 456 $old_edges = array(); 451 457 if ($object->getPHID()) { 452 458 $edge_src = $object->getPHID(); ··· 513 519 return $space_phid; 514 520 } 515 521 case PhabricatorTransactions::TYPE_EDGE: 522 + // See T13082. If this is an inverse edit, the parent editor has 523 + // already populated appropriate transaction values. 524 + if ($this->getIsInverseEdgeEditor()) { 525 + return $xaction->getNewValue(); 526 + } 527 + 516 528 $new_value = $this->getEdgeTransactionNewValue($xaction); 517 529 518 530 $edge_type = $xaction->getMetadataValue('edge:type'); ··· 790 802 $src = $object->getPHID(); 791 803 $const = $xaction->getMetadataValue('edge:type'); 792 804 793 - $type = PhabricatorEdgeType::getByConstant($const); 794 - if ($type->shouldWriteInverseTransactions()) { 795 - $this->applyInverseEdgeTransactions( 796 - $object, 797 - $xaction, 798 - $type->getInverseEdgeConstant()); 799 - } 800 - 801 805 foreach ($new as $dst_phid => $edge) { 802 806 $new[$dst_phid]['src'] = $src; 803 807 } ··· 899 903 900 904 foreach ($xactions as $xaction) { 901 905 $type = $xaction->getTransactionType(); 906 + 907 + // See T13082. When we're writing edges that imply corresponding inverse 908 + // transactions, apply those inverse transactions now. We have to wait 909 + // until the object we're editing (with this editor) has committed its 910 + // transactions to do this. If we don't, the inverse editor may race, 911 + // build a mail before we actually commit this object, and render "alice 912 + // added an edge: Unknown Object". 913 + 914 + if ($type === PhabricatorTransactions::TYPE_EDGE) { 915 + // Don't do anything if we're already an inverse edge editor. 916 + if ($this->getIsInverseEdgeEditor()) { 917 + continue; 918 + } 919 + 920 + $edge_const = $xaction->getMetadataValue('edge:type'); 921 + $edge_type = PhabricatorEdgeType::getByConstant($edge_const); 922 + if ($edge_type->shouldWriteInverseTransactions()) { 923 + $this->applyInverseEdgeTransactions( 924 + $object, 925 + $xaction, 926 + $edge_type->getInverseEdgeConstant()); 927 + } 928 + continue; 929 + } 902 930 903 931 $xtype = $this->getModularTransactionType($type); 904 932 if (!$xtype) { ··· 1503 1531 1504 1532 $expect_value = !$xaction->shouldGenerateOldValue(); 1505 1533 $has_value = $xaction->hasOldValue(); 1534 + 1535 + // See T13082. In the narrow case of applying inverse edge edits, we 1536 + // expect the old value to be populated. 1537 + if ($this->getIsInverseEdgeEditor()) { 1538 + $expect_value = true; 1539 + } 1506 1540 1507 1541 if ($expect_value && !$has_value) { 1508 1542 throw new PhabricatorApplicationTransactionStructureException( ··· 3853 3887 ->withPHIDs($all) 3854 3888 ->execute(); 3855 3889 3890 + $object_phid = $object->getPHID(); 3891 + 3856 3892 foreach ($nodes as $node) { 3857 3893 if (!($node instanceof PhabricatorApplicationTransactionInterface)) { 3858 3894 continue; ··· 3865 3901 continue; 3866 3902 } 3867 3903 3904 + $node_phid = $node->getPHID(); 3868 3905 $editor = $node->getApplicationTransactionEditor(); 3869 3906 $template = $node->getApplicationTransactionTemplate(); 3870 3907 3871 - if (isset($add[$node->getPHID()])) { 3872 - $edge_edit_type = '+'; 3908 + // See T13082. We have to build these transactions with synthetic values 3909 + // because we've already applied the actual edit to the edge database 3910 + // table. If we try to apply this transaction naturally, it will no-op 3911 + // itself because it doesn't have any effect. 3912 + 3913 + $edge_query = id(new PhabricatorEdgeQuery()) 3914 + ->withSourcePHIDs(array($node_phid)) 3915 + ->withEdgeTypes(array($inverse_type)); 3916 + 3917 + $edge_query->execute(); 3918 + 3919 + $edge_phids = $edge_query->getDestinationPHIDs(); 3920 + $edge_phids = array_fuse($edge_phids); 3921 + 3922 + $new_phids = $edge_phids; 3923 + $old_phids = $edge_phids; 3924 + 3925 + if (isset($add[$node_phid])) { 3926 + unset($old_phids[$object_phid]); 3873 3927 } else { 3874 - $edge_edit_type = '-'; 3928 + $old_phids[$object_phid] = $object_phid; 3875 3929 } 3876 3930 3877 3931 $template 3878 3932 ->setTransactionType($xaction->getTransactionType()) 3879 3933 ->setMetadataValue('edge:type', $inverse_type) 3880 - ->setNewValue( 3881 - array( 3882 - $edge_edit_type => array($object->getPHID() => $object->getPHID()), 3883 - )); 3934 + ->setOldValue($old_phids) 3935 + ->setNewValue($new_phids); 3884 3936 3885 3937 $editor 3886 3938 ->setContinueOnNoEffect(true)