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

Give Futures clearer start/end and exception semantics

Summary:
Ref T13555. Currently:

  - If an exception is raised in "start()", the exception state is not set on the future.
  - Futures do not always call "startFuture()" before starting, and do not always call "endFuture()" once they become resolvable.
  - If you start an ExecFuture which immediately fails and then call "getPID()" on it, you get an unclear exception.

Simplify these behaviors:

  - In FutureIterator, only start futures which have not already started.
  - When starting a future on any pathway, run start code.
  - When a future becomes resolvable on any pathway, run end code.
  - Raise a more clear exception when calling "getPID()" on a future with no subprocess.

Test Plan: Faked a failing subprocess with "$proc = null", ran "bin/phd debug taskmaster" etc. Got clearer errors and more consistent future lifecycle workflows.

Maniphest Tasks: T13555

Differential Revision: https://secure.phabricator.com/D21423
This commit is contained in:
epriestley 2020-07-23 08:26:32 -07:00
parent 65cda1596f
commit ceb082ef6b
4 changed files with 94 additions and 52 deletions

View file

@ -14,6 +14,7 @@ abstract class Future extends Phobject {
private $exception; private $exception;
private $futureKey; private $futureKey;
private $serviceProfilerCallID; private $serviceProfilerCallID;
private $raiseExceptionOnStart = true;
private static $nextKey = 1; private static $nextKey = 1;
/** /**
@ -41,7 +42,7 @@ abstract class Future extends Phobject {
'timeout.')); 'timeout.'));
} }
if (!$this->hasResult() && !$this->hasException()) { if (!$this->canResolve()) {
$graph = new FutureIterator(array($this)); $graph = new FutureIterator(array($this));
$graph->resolveAll(); $graph->resolveAll();
} }
@ -53,25 +54,8 @@ abstract class Future extends Phobject {
return $this->getResult(); return $this->getResult();
} }
final public function startFuture() {
if ($this->hasStarted) {
throw new Exception(
pht(
'Future has already started; futures can not start more '.
'than once.'));
}
$this->hasStarted = true;
$this->startServiceProfiler();
$this->updateFuture();
}
final public function updateFuture() { final public function updateFuture() {
if ($this->hasException()) { if ($this->canResolve()) {
return;
}
if ($this->hasResult()) {
return; return;
} }
@ -84,25 +68,6 @@ abstract class Future extends Phobject {
} }
} }
final public function endFuture() {
if (!$this->hasException() && !$this->hasResult()) {
throw new Exception(
pht(
'Trying to end a future which has no exception and no result. '.
'Futures must resolve before they can be ended.'));
}
if ($this->hasEnded) {
throw new Exception(
pht(
'Future has already ended; futures can not end more '.
'than once.'));
}
$this->hasEnded = true;
$this->endServiceProfiler();
}
private function startServiceProfiler() { private function startServiceProfiler() {
// NOTE: This is a soft dependency so that we don't need to build the // NOTE: This is a soft dependency so that we don't need to build the
@ -181,7 +146,24 @@ abstract class Future extends Phobject {
} }
public function start() { public function start() {
$this->isReady(); if ($this->hasStarted) {
throw new Exception(
pht(
'Future has already started; futures can not start more '.
'than once.'));
}
$this->hasStarted = true;
$this->startServiceProfiler();
$this->updateFuture();
if ($this->raiseExceptionOnStart) {
if ($this->hasException()) {
throw $this->getException();
}
}
return $this; return $this;
} }
@ -212,6 +194,8 @@ abstract class Future extends Phobject {
$this->hasResult = true; $this->hasResult = true;
$this->result = $result; $this->result = $result;
$this->endFuture();
return $this; return $this;
} }
@ -219,13 +203,16 @@ abstract class Future extends Phobject {
return $this->hasResult; return $this->hasResult;
} }
final private function setException($exception) { private function setException($exception) {
// NOTE: The parameter may be an Exception or a Throwable. // NOTE: The parameter may be an Exception or a Throwable.
$this->exception = $exception; $this->exception = $exception;
$this->endFuture();
return $this; return $this;
} }
final private function getException() { private function getException() {
return $this->exception; return $this->exception;
} }
@ -254,4 +241,37 @@ abstract class Future extends Phobject {
return $this->futureKey; return $this->futureKey;
} }
final public function setRaiseExceptionOnStart($raise) {
$this->raiseExceptionOnStart = $raise;
return $this;
}
final public function getHasFutureStarted() {
return $this->hasStarted;
}
final public function canResolve() {
if ($this->hasResult()) {
return true;
}
if ($this->hasException()) {
return true;
}
return false;
}
private function endFuture() {
if ($this->hasEnded) {
throw new Exception(
pht(
'Future has already ended; futures can not end more '.
'than once.'));
}
$this->hasEnded = true;
$this->endServiceProfiler();
}
} }

View file

@ -222,12 +222,7 @@ final class FutureIterator
$resolve_key = null; $resolve_key = null;
foreach ($working_set as $future_key => $future) { foreach ($working_set as $future_key => $future) {
if ($future->hasException()) { if ($future->canResolve()) {
$resolve_key = $future_key;
break;
}
if ($future->hasResult()) {
$resolve_key = $future_key; $resolve_key = $future_key;
break; break;
} }
@ -393,7 +388,13 @@ final class FutureIterator
unset($this->wait[$future_key]); unset($this->wait[$future_key]);
$this->work[$future_key] = $future_key; $this->work[$future_key] = $future_key;
$this->futures[$future_key]->startFuture(); $future = $this->futures[$future_key];
if (!$future->getHasFutureStarted()) {
$future
->setRaiseExceptionOnStart(false)
->start();
}
} }
private function moveFutureToDone($future_key) { private function moveFutureToDone($future_key) {
@ -404,8 +405,6 @@ final class FutureIterator
// futures that are ready to go as soon as we can. // futures that are ready to go as soon as we can.
$this->updateWorkingSet(); $this->updateWorkingSet();
$this->futures[$future_key]->endFuture();
} }
/** /**

View file

@ -95,6 +95,18 @@ final class ExecFuture extends PhutilExecutableFuture {
return $status['pid']; return $status['pid'];
} }
public function hasPID() {
if ($this->procStatus) {
return true;
}
if ($this->proc) {
return true;
}
return false;
}
/* -( Configuring Execution )---------------------------------------------- */ /* -( Configuring Execution )---------------------------------------------- */
@ -194,7 +206,7 @@ final class ExecFuture extends PhutilExecutableFuture {
public function readStdout() { public function readStdout() {
if ($this->start) { if ($this->start) {
$this->isReady(); // Sync $this->updateFuture(); // Sync
} }
$result = (string)substr($this->stdout, $this->stdoutPos); $result = (string)substr($this->stdout, $this->stdoutPos);
@ -890,6 +902,17 @@ final class ExecFuture extends PhutilExecutableFuture {
return $this->procStatus; return $this->procStatus;
} }
} }
// See T13555. This may occur if you call "getPID()" on a future which
// exited immediately without ever creating a valid subprocess.
if (!$this->proc) {
throw new Exception(
pht(
'Attempting to get subprocess status in "ExecFuture" with no '.
'valid subprocess.'));
}
$this->procStatus = proc_get_status($this->proc); $this->procStatus = proc_get_status($this->proc);
return $this->procStatus; return $this->procStatus;

View file

@ -104,7 +104,7 @@ final class ArcanistGitCommitGraphQuery
} }
$future = array_pop($this->futures); $future = array_pop($this->futures);
$future->startFuture(); $future->start();
$iterator = id(new LinesOfALargeExecFuture($future)) $iterator = id(new LinesOfALargeExecFuture($future))
->setDelimiter("\1"); ->setDelimiter("\1");