1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

When applying transactions, acquire a read lock sooner

Summary:
Depends on D20461. Ref T13276. Ref T13054.

Currently, we acquire the transaction read lock after populating "old values" in transactions and filtering transactions with no effect.

This isn't early enough to prevent all weird chaotic races: if two processes try to apply a "close revision" transaction at the same time, this can happen:

```
PROCESS A             PROCESS B
Old Value = Open      Old Value = Open
Transaction OK: Yes   Transaction OK: Yes
Acquire Read Lock     Acquire Read Lock
Got Read Lock!        Wait...
Apply Transactions    Wait...
New Value = Closed    Wait...
Release Lock          Wait...
                      Got Read Lock!
                      Apply Transactions
                      New Value = Closed
                      Release Lock
```

That's not great: both processes apply an "Open -> Closed" transaction since this was a valid transaction from the viewpoint of each process when it did the checks.

We actually want this:

```
PROCESS A             PROCESS B
Acquire Read Lock     Acquire Read Lock
Got Read Lock!        Wait...
Old Value = Open      Wait...
Transaction OK: Yes   Wait...
Apply Transactions    Wait...
New Value = Closed    Wait...
Release Lock          Wait...
                      Got Read Lock!
                      >>> Old Value = Closed
                      >>> Transaction Has No Effect!
                      >>> Do Nothing / Abort
                      Release Lock
```

Move the "lock" part up so we do that.

This may cause some kind of weird second-order effects, but T13054 went through pretty cleanly and we have to do this to get correct behavior, so we can survive those if/when they arise.

Test Plan:
  - Added a `sleep(10)` before the lock.
  - Ran `bin/repository message --reparse X` in two console windows, where X is a commit that closes revision Y and Y is open.
    - Before patch: both windows closed the revision and added duplicate transactions.
    - After patch: only one of the processes had an effect.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: jmeador

Maniphest Tasks: T13276, T13054

Differential Revision: https://secure.phabricator.com/D20462
This commit is contained in:
epriestley 2019-04-22 15:13:36 -07:00
parent 42c02557e4
commit b3b1cc64bd

View file

@ -977,91 +977,20 @@ abstract class PhabricatorApplicationTransactionEditor
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
$xactions = $this->newMFATransactions($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
$is_new = ($object->getPHID() === null);
$this->isNewObject = $is_new;
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException($errors);
}
if ($this->raiseWarnings) {
$warnings = array();
foreach ($xactions as $xaction) {
if ($this->hasWarnings($object, $xaction)) {
$warnings[] = $xaction;
}
}
if ($warnings) {
throw new PhabricatorApplicationTransactionWarningException(
$warnings);
}
}
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
// Now that we've merged and combined transactions, check for required
// capabilities. Note that we're doing this before filtering
// transactions: if you try to apply an edit which you do not have
// permission to apply, we want to give you a permissions error even
// if the edit would have no effect.
$this->applyCapabilityChecks($object, $xactions);
$xactions = $this->filterTransactions($object, $xactions);
// If we're attempting to apply transactions, lock and reload the object
// before we go anywhere. If we don't do this at the very beginning, we
// may be looking at an older version of the object when we populate and
// filter the transactions. See PHI1165 for an example.
if (!$is_preview) {
$this->hasRequiredMFA = true;
if ($this->getShouldRequireMFA()) {
$this->requireMFA($object, $xactions);
}
if ($object->getID()) {
if (!$is_new) {
$this->buildOldRecipientLists($object, $xactions);
$object->openTransaction();
@ -1072,16 +1001,102 @@ abstract class PhabricatorApplicationTransactionEditor
$object->reload();
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
try {
$this->object = $object;
$this->xactions = $xactions;
$this->validateEditParameters($object, $xactions);
$xactions = $this->newMFATransactions($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction(
$object,
$type,
$type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions(
$object,
$xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException(
$errors);
}
if ($this->raiseWarnings) {
$warnings = array();
foreach ($xactions as $xaction) {
if ($this->hasWarnings($object, $xaction)) {
$warnings[] = $xaction;
}
}
if ($warnings) {
throw new PhabricatorApplicationTransactionWarningException(
$warnings);
}
}
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
// Now that we've merged and combined transactions, check for required
// capabilities. Note that we're doing this before filtering
// transactions: if you try to apply an edit which you do not have
// permission to apply, we want to give you a permissions error even
// if the edit would have no effect.
$this->applyCapabilityChecks($object, $xactions);
$xactions = $this->filterTransactions($object, $xactions);
if (!$is_preview) {
$this->hasRequiredMFA = true;
if ($this->getShouldRequireMFA()) {
$this->requireMFA($object, $xactions);
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}