Repository: kriswallsmith/spork Branch: master Commit: 530fcf57fce4 Files: 42 Total size: 66.7 KB Directory structure: gitextract_92oy9ow3/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src/ │ └── Spork/ │ ├── Batch/ │ │ ├── BatchJob.php │ │ ├── BatchRunner.php │ │ └── Strategy/ │ │ ├── AbstractStrategy.php │ │ ├── CallbackStrategy.php │ │ ├── ChunkStrategy.php │ │ ├── DoctrineMongoStrategy.php │ │ ├── MongoStrategy.php │ │ ├── StrategyInterface.php │ │ └── ThrottleStrategy.php │ ├── Deferred/ │ │ ├── Deferred.php │ │ ├── DeferredAggregate.php │ │ ├── DeferredInterface.php │ │ └── PromiseInterface.php │ ├── EventDispatcher/ │ │ ├── EventDispatcher.php │ │ ├── EventDispatcherInterface.php │ │ ├── Events.php │ │ └── WrappedEventDispatcher.php │ ├── Exception/ │ │ ├── ForkException.php │ │ ├── ProcessControlException.php │ │ └── UnexpectedTypeException.php │ ├── Factory.php │ ├── Fork.php │ ├── ProcessManager.php │ ├── SharedMemory.php │ └── Util/ │ ├── Error.php │ ├── ExitMessage.php │ └── ThrottleIterator.php └── tests/ ├── Spork/ │ └── Test/ │ ├── Batch/ │ │ └── Strategy/ │ │ ├── ChunkStrategyTest.php │ │ └── MongoStrategyTest.php │ ├── Deferred/ │ │ ├── DeferredAggregateTest.php │ │ └── DeferredTest.php │ ├── ProcessManagerTest.php │ ├── SignalTest.php │ └── Util/ │ └── ThrottleIteratorTest.php └── bootstrap.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ composer.lock composer.phar phpunit.xml vendor/ ================================================ FILE: .travis.yml ================================================ language: php php: - 5.3 - 5.4 - 5.5 before_script: - wget http://getcomposer.org/composer.phar - php composer.phar install --dev script: phpunit --coverage-text ================================================ FILE: CHANGELOG.md ================================================ ## 0.3 (May 18, 2015) * Changed ProcessManager constructor to accept new Factory class as second argument * Use shared memory for interprocess communications (@MattJaniszewski) * Added progress callbacks to Deferred * Added serializable objects for exit and error messages ================================================ FILE: LICENSE ================================================ Copyright (c) 2012-2013 OpenSky Project Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://secure.travis-ci.org/kriswallsmith/spork.png?branch=master)](http://travis-ci.org/kriswallsmith/spork) Spork: PHP on a Fork -------------------- ```php fork(function() { // do something in another process! return 'Hello from '.getmypid(); })->then(function(Spork\Fork $fork) { // do something in the parent process when it's done! echo "{$fork->getPid()} says '{$fork->getResult()}'\n"; }); ``` ### Example: Upload images to your CDN Feed an iterator into the process manager and it will break the job into multiple batches and spread them across many processes. ```php process($files, function(SplFileInfo $file) { // upload this file }); ``` ================================================ FILE: composer.json ================================================ { "name": "kriswallsmith/spork", "description": "Asynchronous PHP", "homepage": "https://github.com/kriswallsmith/spork", "type": "library", "license": "MIT", "authors": [ { "name": "Kris Wallsmith", "email": "kris.wallsmith@gmail.com", "homepage": "http://kriswallsmith.net/" } ], "require": { "php": ">=5.3.0", "ext-pcntl": "*", "ext-posix": "*", "ext-shmop": "*", "symfony/event-dispatcher": "*" }, "autoload": { "psr-0": { "Spork": "src/" } } } ================================================ FILE: phpunit.xml.dist ================================================ ./tests/Spork/Test/ ./src/Spork/ ================================================ FILE: src/Spork/Batch/BatchJob.php ================================================ manager = $manager; $this->data = $data; $this->strategy = $strategy ?: new ChunkStrategy(); $this->name = ''; } public function setName($name) { $this->name = $name; return $this; } public function setStrategy(StrategyInterface $strategy) { $this->strategy = $strategy; return $this; } public function setData($data) { $this->data = $data; return $this; } public function setCallback($callback) { if (!is_callable($callback)) { throw new UnexpectedTypeException($callback, 'callable'); } $this->callback = $callback; return $this; } public function execute($callback = null) { if (null !== $callback) { $this->setCallback($callback); } return $this->manager->fork($this)->setName($this->name.' batch'); } /** * Runs in a child process. * * @see execute() */ public function __invoke() { $forks = array(); foreach ($this->strategy->createBatches($this->data) as $index => $batch) { $forks[] = $this->manager ->fork($this->strategy->createRunner($batch, $this->callback)) ->setName(sprintf('%s batch #%d', $this->name, $index)) ; } // block until all forks have exited $this->manager->wait(); $results = array(); foreach ($forks as $fork) { $results = array_merge($results, $fork->getResult()); } return $results; } } ================================================ FILE: src/Spork/Batch/BatchRunner.php ================================================ batch = $batch; $this->callback = $callback; } public function __invoke(SharedMemory $shm) { // lazy batch... if ($this->batch instanceof \Closure) { $this->batch = call_user_func($this->batch); } $results = array(); foreach ($this->batch as $index => $item) { $results[$index] = call_user_func($this->callback, $item, $index, $this->batch, $shm); } return $results; } } ================================================ FILE: src/Spork/Batch/Strategy/AbstractStrategy.php ================================================ callback = $callback; } public function createBatches($data) { return call_user_func($this->callback, $data); } } ================================================ FILE: src/Spork/Batch/Strategy/ChunkStrategy.php ================================================ forks = $forks; $this->preserveKeys = $preserveKeys; } public function createBatches($data) { if (!is_array($data) && !$data instanceof \Traversable) { throw new UnexpectedTypeException($data, 'array or Traversable'); } if ($data instanceof \Traversable) { $data = iterator_to_array($data); } $size = ceil(count($data) / $this->forks); return array_chunk($data, $size, $this->preserveKeys); } } ================================================ FILE: src/Spork/Batch/Strategy/DoctrineMongoStrategy.php ================================================ addListener(Events::PRE_FORK, array($mongo, 'close')); */ class MongoStrategy extends AbstractStrategy { const DATA_CLASS = 'MongoCursor'; private $size; private $skip; /** * Constructor. * * @param integer $size The number of batches to create * @param integer $skip The number of documents to skip */ public function __construct($size = 3, $skip = 0) { $this->size = $size; $this->skip = $skip; } public function createBatches($cursor) { $expected = static::DATA_CLASS; if (!$cursor instanceof $expected) { throw new UnexpectedTypeException($cursor, $expected); } $skip = $this->skip; $limit = ceil(($cursor->count(true) - $skip) / $this->size); $batches = array(); for ($i = 0; $i < $this->size; $i++) { $batches[] = function() use($cursor, $skip, $i, $limit) { return $cursor->skip($skip + $i * $limit)->limit($limit); }; } return $batches; } } ================================================ FILE: src/Spork/Batch/Strategy/StrategyInterface.php ================================================ delegate = $delegate; $this->threshold = $threshold; } public function createBatches($data) { $batches = $this->delegate->createBatches($data); // wrap each batch in the throttle iterator foreach ($batches as $i => $batch) { $batches[$i] = new ThrottleIterator($batch, $this->threshold); } return $batches; } public function createRunner($batch, $callback) { return $this->delegate->createRunner($batch, $callback); } } ================================================ FILE: src/Spork/Deferred/Deferred.php ================================================ state = DeferredInterface::STATE_PENDING; $this->progressCallbacks = array(); $this->alwaysCallbacks = array(); $this->doneCallbacks = array(); $this->failCallbacks = array(); } public function getState() { return $this->state; } public function progress($progress) { if (!is_callable($progress)) { throw new UnexpectedTypeException($progress, 'callable'); } $this->progressCallbacks[] = $progress; return $this; } public function always($always) { if (!is_callable($always)) { throw new UnexpectedTypeException($always, 'callable'); } switch ($this->state) { case DeferredInterface::STATE_PENDING: $this->alwaysCallbacks[] = $always; break; default: call_user_func_array($always, $this->callbackArgs); break; } return $this; } public function done($done) { if (!is_callable($done)) { throw new UnexpectedTypeException($done, 'callable'); } switch ($this->state) { case DeferredInterface::STATE_PENDING: $this->doneCallbacks[] = $done; break; case DeferredInterface::STATE_RESOLVED: call_user_func_array($done, $this->callbackArgs); } return $this; } public function fail($fail) { if (!is_callable($fail)) { throw new UnexpectedTypeException($fail, 'callable'); } switch ($this->state) { case DeferredInterface::STATE_PENDING: $this->failCallbacks[] = $fail; break; case DeferredInterface::STATE_REJECTED: call_user_func_array($fail, $this->callbackArgs); break; } return $this; } public function then($done, $fail = null) { $this->done($done); if ($fail) { $this->fail($fail); } return $this; } public function notify() { if (DeferredInterface::STATE_PENDING !== $this->state) { throw new \LogicException('Cannot notify a deferred object that is no longer pending'); } $args = func_get_args(); foreach ($this->progressCallbacks as $func) { call_user_func_array($func, $args); } return $this; } public function resolve() { if (DeferredInterface::STATE_REJECTED === $this->state) { throw new \LogicException('Cannot resolve a deferred object that has already been rejected'); } if (DeferredInterface::STATE_RESOLVED === $this->state) { return $this; } $this->state = DeferredInterface::STATE_RESOLVED; $this->callbackArgs = func_get_args(); while ($func = array_shift($this->alwaysCallbacks)) { call_user_func_array($func, $this->callbackArgs); } while ($func = array_shift($this->doneCallbacks)) { call_user_func_array($func, $this->callbackArgs); } return $this; } public function reject() { if (DeferredInterface::STATE_RESOLVED === $this->state) { throw new \LogicException('Cannot reject a deferred object that has already been resolved'); } if (DeferredInterface::STATE_REJECTED === $this->state) { return $this; } $this->state = DeferredInterface::STATE_REJECTED; $this->callbackArgs = func_get_args(); while ($func = array_shift($this->alwaysCallbacks)) { call_user_func_array($func, $this->callbackArgs); } while ($func = array_shift($this->failCallbacks)) { call_user_func_array($func, $this->callbackArgs); } return $this; } } ================================================ FILE: src/Spork/Deferred/DeferredAggregate.php ================================================ children = $children; $this->delegate = new Deferred(); // connect to each child foreach ($this->children as $child) { $child->always(array($this, 'tick')); } // always tick once now $this->tick(); } public function getState() { return $this->delegate->getState(); } public function getChildren() { return $this->children; } public function progress($progress) { $this->delegate->progress($progress); return $this; } public function always($always) { $this->delegate->always($always); return $this; } public function done($done) { $this->delegate->done($done); return $this; } public function fail($fail) { $this->delegate->fail($fail); return $this; } public function then($done, $fail = null) { $this->delegate->then($done, $fail); return $this; } public function tick() { $pending = count($this->children); foreach ($this->children as $child) { switch ($child->getState()) { case PromiseInterface::STATE_REJECTED: $this->delegate->reject($this); return; case PromiseInterface::STATE_RESOLVED: --$pending; break; } } if (!$pending) { $this->delegate->resolve($this); } } } ================================================ FILE: src/Spork/Deferred/DeferredInterface.php ================================================ dispatch('spork.signal.'.$signal); } public function addSignalListener($signal, $callable, $priority = 0) { $this->addListener('spork.signal.'.$signal, $callable, $priority); pcntl_signal($signal, array($this, 'dispatchSignal')); } public function removeSignalListener($signal, $callable) { $this->removeListener('spork.signal.'.$signal, $callable); } } ================================================ FILE: src/Spork/EventDispatcher/EventDispatcherInterface.php ================================================ delegate = $delegate; } public function dispatchSignal($signal) { $this->delegate->dispatch('spork.signal.'.$signal); } public function addSignalListener($signal, $callable, $priority = 0) { $this->delegate->addListener('spork.signal.'.$signal, $callable, $priority); pcntl_signal($signal, array($this, 'dispatchSignal')); } public function removeSignalListener($signal, $callable) { $this->delegate->removeListener('spork.signal.'.$signal, $callable); } public function dispatch($eventName, Event $event = null) { return $this->delegate->dispatch($eventName, $event); } public function addListener($eventName, $listener, $priority = 0) { $this->delegate->addListener($eventName, $listener, $priority); } public function addSubscriber(EventSubscriberInterface $subscriber) { $this->delegate->addSubscriber($subscriber); } public function removeListener($eventName, $listener) { $this->delegate->removeListener($eventName, $listener); } public function removeSubscriber(EventSubscriberInterface $subscriber) { $this->delegate->removeSubscriber($subscriber); } public function getListeners($eventName = null) { return $this->delegate->getListeners($eventName); } public function hasListeners($eventName = null) { return $this->delegate->hasListeners($eventName); } } ================================================ FILE: src/Spork/Exception/ForkException.php ================================================ name = $name; $this->pid = $pid; $this->error = $error; if ($error) { if (__CLASS__ === $error->getClass()) { parent::__construct(sprintf('%s via "%s" fork (%d)', $error->getMessage(), $name, $pid)); } else { parent::__construct(sprintf( '%s (%d) thrown in "%s" fork (%d): "%s" (%s:%d)', $error->getClass(), $error->getCode(), $name, $pid, $error->getMessage(), $error->getFile(), $error->getLine() )); } } else { parent::__construct(sprintf('An unknown error occurred in "%s" fork (%d)', $name, $pid)); } } public function getPid() { return $this->pid; } public function getError() { return $this->error; } } ================================================ FILE: src/Spork/Exception/ProcessControlException.php ================================================ defer = new Deferred(); $this->pid = $pid; $this->shm = $shm; $this->debug = $debug; $this->name = ''; } /** * Assign a name to the current fork (useful for debugging). */ public function setName($name) { $this->name = $name; return $this; } public function getPid() { return $this->pid; } public function wait($hang = true) { if ($this->isExited()) { return $this; } if (-1 === $pid = pcntl_waitpid($this->pid, $status, ($hang ? 0 : WNOHANG) | WUNTRACED)) { throw new ProcessControlException('Error while waiting for process '.$this->pid); } if ($this->pid === $pid) { $this->processWaitStatus($status); } return $this; } /** * Processes a status value retrieved while waiting for this fork to exit. */ public function processWaitStatus($status) { if ($this->isExited()) { throw new \LogicException('Cannot set status on an exited fork'); } $this->status = $status; if ($this->isExited()) { $this->receive(); $this->isSuccessful() ? $this->resolve() : $this->reject(); if ($this->debug && (!$this->isSuccessful() || $this->getError())) { throw new ForkException($this->name, $this->pid, $this->getError()); } } } public function receive() { $messages = array(); foreach ($this->shm->receive() as $message) { if ($message instanceof ExitMessage) { $this->message = $message; } else { $messages[] = $message; } } return $messages; } public function kill($signal = SIGINT) { if (false === $this->shm->signal($signal)) { throw new ProcessControlException('Unable to send signal'); } return $this; } public function getResult() { if ($this->message) { return $this->message->getResult(); } } public function getOutput() { if ($this->message) { return $this->message->getOutput(); } } public function getError() { if ($this->message) { return $this->message->getError(); } } public function isSuccessful() { return 0 === $this->getExitStatus(); } public function isExited() { return null !== $this->status && pcntl_wifexited($this->status); } public function isStopped() { return null !== $this->status && pcntl_wifstopped($this->status); } public function isSignaled() { return null !== $this->status && pcntl_wifsignaled($this->status); } public function getExitStatus() { if (null !== $this->status) { return pcntl_wexitstatus($this->status); } } public function getTermSignal() { if (null !== $this->status) { return pcntl_wtermsig($this->status); } } public function getStopSignal() { if (null !== $this->status) { return pcntl_wstopsig($this->status); } } public function getState() { return $this->defer->getState(); } public function progress($progress) { $this->defer->progress($progress); return $this; } public function always($always) { $this->defer->always($always); return $this; } public function done($done) { $this->defer->done($done); return $this; } public function fail($fail) { $this->defer->fail($fail); return $this; } public function then($done, $fail = null) { $this->defer->then($done, $fail); return $this; } public function notify() { $args = func_get_args(); array_unshift($args, $this); call_user_func_array(array($this->defer, 'notify'), $args); return $this; } public function resolve() { $args = func_get_args(); array_unshift($args, $this); call_user_func_array(array($this->defer, 'resolve'), $args); return $this; } public function reject() { $args = func_get_args(); array_unshift($args, $this); call_user_func_array(array($this->defer, 'reject'), $args); return $this; } } ================================================ FILE: src/Spork/ProcessManager.php ================================================ dispatcher = $dispatcher ?: new EventDispatcher(); $this->factory = $factory ?: new Factory(); $this->debug = $debug; $this->zombieOkay = false; $this->forks = array(); } public function __destruct() { if (!$this->zombieOkay) { $this->wait(); } } public function getEventDispatcher() { return $this->dispatcher; } public function addListener($eventName, $listener, $priority = 0) { if (is_integer($eventName)) { $this->dispatcher->addSignalListener($eventName, $listener, $priority); } else { $this->dispatcher->addListener($eventName, $listener, $priority); } } public function setDebug($debug) { $this->debug = $debug; } public function zombieOkay($zombieOkay = true) { $this->zombieOkay = $zombieOkay; } public function createBatchJob($data = null, StrategyInterface $strategy = null) { return $this->factory->createBatchJob($this, $data, $strategy); } public function process($data, $callable, StrategyInterface $strategy = null) { return $this->createBatchJob($data, $strategy)->execute($callable); } /** * Forks something into another process and returns a deferred object. */ public function fork($callable) { if (!is_callable($callable)) { throw new UnexpectedTypeException($callable, 'callable'); } // allow the system to cleanup before forking $this->dispatcher->dispatch(Events::PRE_FORK); if (-1 === $pid = pcntl_fork()) { throw new ProcessControlException('Unable to fork a new process'); } if (0 === $pid) { // reset the list of child processes $this->forks = array(); // setup the shared memory $shm = $this->factory->createSharedMemory(null, $this->signal); $message = new ExitMessage(); // phone home on shutdown register_shutdown_function(function() use($shm, $message) { $status = null; try { $shm->send($message, false); } catch (\Exception $e) { // probably an error serializing the result $message->setResult(null); $message->setError(Error::fromException($e)); $shm->send($message, false); exit(2); } }); // dispatch an event so the system knows it's in a new process $this->dispatcher->dispatch(Events::POST_FORK); if (!$this->debug) { ob_start(); } try { $result = call_user_func($callable, $shm); $message->setResult($result); $status = is_integer($result) ? $result : 0; } catch (\Exception $e) { $message->setError(Error::fromException($e)); $status = 1; } if (!$this->debug) { $message->setOutput(ob_get_clean()); } exit($status); } // connect to shared memory $shm = $this->factory->createSharedMemory($pid); return $this->forks[$pid] = $this->factory->createFork($pid, $shm, $this->debug); } public function monitor($signal = SIGUSR1) { $this->signal = $signal; $this->dispatcher->addSignalListener($signal, array($this, 'check')); } public function check() { foreach ($this->forks as $fork) { foreach ($fork->receive() as $message) { $fork->notify($message); } } } public function wait($hang = true) { foreach ($this->forks as $fork) { $fork->wait($hang); } } public function waitForNext($hang = true) { if (-1 === $pid = pcntl_wait($status, ($hang ? WNOHANG : 0) | WUNTRACED)) { throw new ProcessControlException('Error while waiting for next fork to exit'); } if (isset($this->forks[$pid])) { $this->forks[$pid]->processWaitStatus($status); return $this->forks[$pid]; } } public function waitFor($pid, $hang = true) { if (!isset($this->forks[$pid])) { throw new \InvalidArgumentException('There is no fork with PID '.$pid); } return $this->forks[$pid]->wait($hang); } /** * Sends a signal to all forks. */ public function killAll($signal = SIGINT) { foreach ($this->forks as $fork) { $fork->kill($signal); } } } ================================================ FILE: src/Spork/SharedMemory.php ================================================ pid = $pid; $this->ppid = $ppid; $this->signal = $signal; } /** * Reads all messages from shared memory. * * @return array An array of messages */ public function receive() { if (($shmId = @shmop_open($this->pid, 'a', 0, 0)) > 0) { $serializedMessages = shmop_read($shmId, 0, shmop_size($shmId)); shmop_delete($shmId); shmop_close($shmId); return unserialize($serializedMessages); } return array(); } /** * Writes a message to the shared memory. * * @param mixed $message The message to send * @param integer $signal The signal to send afterward * @param integer $pause The number of microseconds to pause after signalling */ public function send($message, $signal = null, $pause = 500) { $messageArray = array(); if (($shmId = @shmop_open($this->pid, 'a', 0, 0)) > 0) { // Read any existing messages in shared memory $readMessage = shmop_read($shmId, 0, shmop_size($shmId)); $messageArray[] = unserialize($readMessage); shmop_delete($shmId); shmop_close($shmId); } // Add the current message to the end of the array, and serialize it $messageArray[] = $message; $serializedMessage = serialize($messageArray); // Write new serialized message to shared memory $shmId = shmop_open($this->pid, 'c', 0644, strlen($serializedMessage)); if (!$shmId) { throw new ProcessControlException(sprintf('Not able to create shared memory segment for PID: %s', $this->pid)); } else if (shmop_write($shmId, $serializedMessage, 0) !== strlen($serializedMessage)) { throw new ProcessControlException( sprintf('Not able to write message to shared memory segment for segment ID: %s', $shmId) ); } if (false === $signal) { return; } $this->signal($signal ?: $this->signal); usleep($pause); } /** * Sends a signal to the other process. */ public function signal($signal) { $pid = null === $this->ppid ? $this->pid : $this->ppid; return posix_kill($pid, $signal); } } ================================================ FILE: src/Spork/Util/Error.php ================================================ setClass(get_class($e)); $flat->setMessage($e->getMessage()); $flat->setFile($e->getFile()); $flat->setLine($e->getLine()); $flat->setCode($e->getCode()); return $flat; } public function getClass() { return $this->class; } public function setClass($class) { $this->class = $class; } public function getMessage() { return $this->message; } public function setMessage($message) { $this->message = $message; } public function getFile() { return $this->file; } public function setFile($file) { $this->file = $file; } public function getLine() { return $this->line; } public function setLine($line) { $this->line = $line; } public function getCode() { return $this->code; } public function setCode($code) { $this->code = $code; } public function serialize() { return serialize(array( $this->class, $this->message, $this->file, $this->line, $this->code, )); } public function unserialize($str) { list( $this->class, $this->message, $this->file, $this->line, $this->code ) = unserialize($str); } } ================================================ FILE: src/Spork/Util/ExitMessage.php ================================================ result; } public function setResult($result) { $this->result = $result; } public function getOutput() { return $this->output; } public function setOutput($output) { $this->output = $output; } public function getError() { return $this->error; } public function setError(Error $error) { $this->error = $error; } public function serialize() { return serialize(array( $this->result, $this->output, $this->error, )); } public function unserialize($str) { list( $this->result, $this->output, $this->error ) = unserialize($str); } } ================================================ FILE: src/Spork/Util/ThrottleIterator.php ================================================ inner = $inner; $this->threshold = $threshold; } /** * Attempts to lazily resolve the supplied inner to an instance of Iterator. */ public function getInnerIterator() { if (is_callable($this->inner)) { // callable $this->inner = call_user_func($this->inner); } if (is_array($this->inner)) { // array $this->inner = new \ArrayIterator($this->inner); } elseif ($this->inner instanceof \IteratorAggregate) { // IteratorAggregate while ($this->inner instanceof \IteratorAggregate) { $this->inner = $this->inner->getIterator(); } } if (!$this->inner instanceof \Iterator) { throw new UnexpectedTypeException($this->inner, 'Iterator'); } return $this->inner; } public function current() { // only throttle every 5s if ($this->lastThrottle < time() - 5) { $this->throttle(); } return $this->getInnerIterator()->current(); } public function key() { return $this->getInnerIterator()->key(); } public function next() { return $this->getInnerIterator()->next(); } public function rewind() { return $this->getInnerIterator()->rewind(); } public function valid() { return $this->getInnerIterator()->valid(); } protected function getLoad() { list($load) = sys_getloadavg(); return $load; } protected function sleep($period) { sleep($period); } private function throttle($period = 1) { $this->lastThrottle = time(); if ($this->threshold <= $this->getLoad()) { $this->sleep($period); $this->throttle($period * 2); } } } ================================================ FILE: tests/Spork/Test/Batch/Strategy/ChunkStrategyTest.php ================================================ createBatches(range(1, 100)); $this->assertEquals(count($expectedCounts), count($batches)); foreach ($batches as $i => $batch) { $this->assertCount($expectedCounts[$i], $batch); } } public function provideNumber() { return array( array(1, array(100)), array(2, array(50, 50)), array(3, array(34, 34, 32)), array(4, array(25, 25, 25, 25)), array(5, array(20, 20, 20, 20, 20)), ); } } ================================================ FILE: tests/Spork/Test/Batch/Strategy/MongoStrategyTest.php ================================================ markTestSkipped('Mongo extension is not loaded'); } try { $this->mongo = new \MongoClient(); } catch (\MongoConnectionException $e) { $this->markTestSkipped($e->getMessage()); } $this->manager = new ProcessManager(); $this->manager->setDebug(true); // close the connection prior to forking $mongo = $this->mongo; $this->manager->addListener(Events::PRE_FORK, function() use($mongo) { $mongo->close(); }); } protected function tearDown() { if ($this->mongo) { $this->mongo->close(); } unset($this->mongo, $this->manager); } public function testBatchJob() { $coll = $this->mongo->spork->widgets; $coll->remove(); $coll->batchInsert(array( array('name' => 'Widget 1'), array('name' => 'Widget 2'), array('name' => 'Widget 3'), )); $this->manager->createBatchJob($coll->find(), new MongoStrategy()) ->execute(function($doc) use($coll) { $coll->update( array('_id' => $doc['_id']), array('$set' => array('seen' => true)) ); }); $this->manager->wait(); foreach ($coll->find() as $doc) { $this->assertArrayHasKey('seen', $doc); } } } ================================================ FILE: tests/Spork/Test/Deferred/DeferredAggregateTest.php ================================================ setExpectedException('Spork\Exception\UnexpectedTypeException', 'PromiseInterface'); $defer = new DeferredAggregate(array('asdf')); } public function testNoChildren() { $defer = new DeferredAggregate(array()); $log = array(); $defer->done(function() use(& $log) { $log[] = 'done'; }); $this->assertEquals(array('done'), $log); } public function testResolvedChildren() { $child = new Deferred(); $child->resolve(); $defer = new DeferredAggregate(array($child)); $log = array(); $defer->done(function() use(& $log) { $log[] = 'done'; }); $this->assertEquals(array('done'), $log); } public function testResolution() { $child1 = new Deferred(); $child2 = new Deferred(); $defer = new DeferredAggregate(array($child1, $child2)); $log = array(); $defer->done(function() use(& $log) { $log[] = 'done'; }); $this->assertEquals(array(), $log); $child1->resolve(); $this->assertEquals(array(), $log); $child2->resolve(); $this->assertEquals(array('done'), $log); } public function testRejection() { $child1 = new Deferred(); $child2 = new Deferred(); $child3 = new Deferred(); $defer = new DeferredAggregate(array($child1, $child2, $child3)); $log = array(); $defer->then(function() use(& $log) { $log[] = 'done'; }, function() use(& $log) { $log[] = 'fail'; }); $this->assertEquals(array(), $log); $child1->resolve(); $this->assertEquals(array(), $log); $child2->reject(); $this->assertEquals(array('fail'), $log); $child3->resolve(); $this->assertEquals(array('fail'), $log); } public function testNested() { $child1a = new Deferred(); $child1b = new Deferred(); $child1 = new DeferredAggregate(array($child1a, $child1b)); $child2 = new Deferred(); $defer = new DeferredAggregate(array($child1, $child2)); $child1a->resolve(); $child1b->resolve(); $child2->resolve(); $this->assertEquals('resolved', $defer->getState()); } public function testFail() { $child = new Deferred(); $defer = new DeferredAggregate(array($child)); $log = array(); $defer->fail(function() use(& $log) { $log[] = 'fail'; }); $child->reject(); $this->assertEquals(array('fail'), $log); } } ================================================ FILE: tests/Spork/Test/Deferred/DeferredTest.php ================================================ defer = new Deferred(); } protected function tearDown() { unset($this->defer); } /** * @dataProvider getMethodAndKey */ public function testCallbackOrder($method, $expected) { $log = array(); $this->defer->always(function() use(& $log) { $log[] = 'always'; $log[] = func_get_args(); })->done(function() use(& $log) { $log[] = 'done'; $log[] = func_get_args(); })->fail(function() use(& $log) { $log[] = 'fail'; $log[] = func_get_args(); }); $this->defer->$method(1, 2, 3); $this->assertEquals(array( 'always', array(1, 2, 3), $expected, array(1, 2, 3), ), $log); } /** * @dataProvider getMethodAndKey */ public function testThen($method, $expected) { $log = array(); $this->defer->then(function() use(& $log) { $log[] = 'done'; }, function() use(& $log) { $log[] = 'fail'; }); $this->defer->$method(); $this->assertEquals(array($expected), $log); } /** * @dataProvider getMethod */ public function testMultipleResolve($method) { $log = array(); $this->defer->always(function() use(& $log) { $log[] = 'always'; }); $this->defer->$method(); $this->defer->$method(); $this->assertEquals(array('always'), $log); } /** * @dataProvider getMethodAndInvalid */ public function testInvalidResolve($method, $invalid) { $this->setExpectedException('LogicException', 'that has already been'); $this->defer->$method(); $this->defer->$invalid(); } /** * @dataProvider getMethodAndQueue */ public function testAlreadyResolved($resolve, $queue, $expect = true) { // resolve the object $this->defer->$resolve(); $log = array(); $this->defer->$queue(function() use(& $log, $queue) { $log[] = $queue; }); $this->assertEquals($expect ? array($queue) : array(), $log); } /** * @dataProvider getMethodAndInvalidCallback */ public function testInvalidCallback($method, $invalid) { $this->setExpectedException('Spork\Exception\UnexpectedTypeException', 'callable'); $this->defer->$method($invalid); } // providers public function getMethodAndKey() { return array( array('resolve', 'done'), array('reject', 'fail'), ); } public function getMethodAndInvalid() { return array( array('resolve', 'reject'), array('reject', 'resolve'), ); } public function getMethodAndQueue() { return array( array('resolve', 'always'), array('resolve', 'done'), array('resolve', 'fail', false), array('reject', 'always'), array('reject', 'done', false), array('reject', 'fail'), ); } public function getMethodAndInvalidCallback() { return array( array('always', 'foo!'), array('always', array('foo!')), array('done', 'foo!'), array('done', array('foo!')), array('fail', 'foo!'), array('fail', array('foo!')), ); } public function getMethod() { return array( array('resolve'), array('reject'), ); } } ================================================ FILE: tests/Spork/Test/ProcessManagerTest.php ================================================ manager = new ProcessManager(); } protected function tearDown() { unset($this->manager); } public function testDoneCallbacks() { $success = null; $fork = $this->manager->fork(function() { echo 'output'; return 'result'; })->done(function() use(& $success) { $success = true; })->fail(function() use(& $success) { $success = false; }); $this->manager->wait(); $this->assertTrue($success); $this->assertEquals('output', $fork->getOutput()); $this->assertEquals('result', $fork->getResult()); } public function testFailCallbacks() { $success = null; $fork = $this->manager->fork(function() { throw new \Exception('child error'); })->done(function() use(& $success) { $success = true; })->fail(function() use(& $success) { $success = false; }); $this->manager->wait(); $this->assertFalse($success); $this->assertNotEmpty($fork->getError()); } public function testObjectReturn() { $fork = $this->manager->fork(function() { return new Unserializable(); }); $this->manager->wait(); $this->assertNull($fork->getResult()); $this->assertFalse($fork->isSuccessful()); } public function testBatchProcessing() { $expected = range(100, 109); $fork = $this->manager->process($expected, function($item) { return $item; }); $this->manager->wait(); $this->assertEquals($expected, $fork->getResult()); } /** * Test batch processing with return values containing a newline character */ public function testBatchProcessingWithNewlineReturnValues() { $range = range(100, 109); $expected = array ( 0 => "SomeString\n100", 1 => "SomeString\n101", 2 => "SomeString\n102", 3 => "SomeString\n103", 4 => "SomeString\n104", 5 => "SomeString\n105", 6 => "SomeString\n106", 7 => "SomeString\n107", 8 => "SomeString\n108", 9 => "SomeString\n109", ); $this->manager->setDebug(true); $fork = $this->manager->process($range, function($item) { return "SomeString\n$item"; }); $this->manager->wait(); $this->assertEquals($expected, $fork->getResult()); } /** * Data provider for `testLargeBatchProcessing()` * * @return array */ public function batchProvider() { return array( array(10), array(1000), array(6941), array(6942), array(6000), array(10000), array(20000), ); } /** * Test large batch sizes * * @dataProvider batchProvider */ public function testLargeBatchProcessing($rangeEnd) { $expected = array_fill(0, $rangeEnd, null); /** @var Fork $fork */ $fork = $this->manager->process($expected, function($item) { return $item; }); $this->manager->wait(); $this->assertEquals($expected, $fork->getResult()); } } class Unserializable { public function __sleep() { throw new \Exception('Hey, don\'t serialize me!'); } } ================================================ FILE: tests/Spork/Test/SignalTest.php ================================================ manager = new ProcessManager(); } protected function tearDown() { $this->manager = null; } public function testSignalParent() { $signaled = false; $this->manager->addListener(SIGUSR1, function() use(& $signaled) { $signaled = true; }); $this->manager->fork(function($sharedMem) { $sharedMem->signal(SIGUSR1); }); $this->manager->wait(); $this->assertTrue($signaled); } } ================================================ FILE: tests/Spork/Test/Util/ThrottleIteratorTest.php ================================================ iterator = new ThrottleIteratorStub(array(1, 2, 3, 4, 5), 3); $this->iterator->loads = array(4, 4, 4, 1, 1); } protected function tearDown() { unset($this->iterator); } public function testIteration() { iterator_to_array($this->iterator); $this->assertEquals(array(1, 2, 4), $this->iterator->sleeps); } } class ThrottleIteratorStub extends ThrottleIterator { public $loads = array(); public $sleeps = array(); protected function getLoad() { return (integer) array_shift($this->loads); } protected function sleep($period) { $this->sleeps[] = $period; } } ================================================ FILE: tests/bootstrap.php ================================================ add('Spork\Test', __DIR__);